Arjen Wiersma

programming

TLDR: I created an interactive chart of editors, see it in my repository.

Today the news dropped that Jetbrains is cancelling its “new” editor, Fleet. Not that I use the product, but it got me thinking about editors (again). I love editors, specifically ones that I can change and bend to my will, such as Emacs.

In my career I have used many different editors. I started on a Laser VTech 310 in Basic 2.0 and from there I transitioned into the world of Pascal and C/C++ to end up on the JVM with Java and Clojure. During that time I have seen many editors come and go in the work field.

Reminiscing about the editors that I have seen made me think about creating a sort of family tree of editors, starting from where I started and then adding editors from the different domain in which I have been active. It kinda got out of hand, just check out this picture:

The map op editors

I started with my trusty Emacs and its friend (neo)vi(m). Alongside staples such as IntelliJ IDEA and VS Code. Using some LLM magic and Wikipedia knowledge I started assembling lineage of the various products and influence of different editors on eachother. For instance, did you know that Lighttable introduced some of the web based editing concepts we now see in Electron? Or that Visual Age was rebuilt into Eclipse, from which modern day editors take a lot of lessons?

I added several neat features to the graph. First you can hover on nodes and vertexes to show you some more information on the editor or its connection.

When you hover

Each node can also be clicked, this will lock the popup and will allow you to click the link(s) in the popup. I want to collect some personal stories for different editors, or perhaps link to some form of codified history on the particular editor.

When you click

You might also wonder about the big blobs on the graph. While researching I found several names to pop up in a lot of different editors, Anders, Erich, Molenaar and the good people of Jetbrains, so I mapped out their influence in the editor landscape. I found the influence that Anders Hejlsberg has had extremely interesting, it basically traces from the start of the code editor (Turbo Pascal, Delphi) all the way to VS Code (Typescript).

If you have any corrections or editor/concepts/stories to add, drop me a line on Mastodon.

#programming

Day 5 of solving {{< backlink “aoc” “The Advent of Code” >}} in {{< backlink “clojure” “Clojure” >}}.

It was a hectic day, and it is not even over yet. I had multiple trips to the vet while trying to get some work done. To relax a bit I worked on the puzzle of the day. In hindsight I was way too cautious with my solution, but then again, it is to solve a puzzle :D

The first thing to figure out is that a list of numbers is in a collection of ranges. Reading in the file and parsing it to numbers is something that has been done the entire week already, then it is a matter of taking the list of ingredients and mapping over the inventory, filtering out the ones that are not in range.

For part 2 the question becomes how many numbers are in the ranges, and this is a dead give away for something that is so huge that you can not just brute force it. The ranges can overlap, so first thing is to merge adjacent or overlapping ranges. My solution is a bit convoluted, but it works. Then it is a matter of getting the size of each range and adding it up.

(ns day5
  (:require
   [clojure.string :as str]))

(defn parse-input [input]
  (let [[available ingredients]
        (-> input
            slurp
            (str/split #"\n\n"))
        inventory (mapv parse-long (str/split-lines ingredients))
        cleaned (->> available
                     str/split-lines
                     (map #(str/split % #"-"))
                     (mapv (partial mapv #(-> % parse-long))))]
    [cleaned inventory]))

(defn part1 []
  (let [[inventory ingredients] (parse-input "resources/5.in")]
    (->> ingredients
         (map
          #(filter (fn [[min max]]
                     (if (and (>= % min) (<= % max))
                       %
                       nil)) inventory))
         (filter seq)
         count)))

(defn should-merge? [[_ end] [start _]]
  (<= start (inc end)))

(defn merge-two-ranges [[start1 end1] [start2 end2]]
  [(min start1 start2) (max end1 end2)])

(defn merge-overlapping [first-range remaining]
  (let [overlapping (filter (partial should-merge? first-range) remaining)
        merged (reduce merge-two-ranges first-range overlapping)
        unmerged (remove (set overlapping) remaining)]
    [merged unmerged]))

(defn merge-pass [ranges]
  (if (empty? ranges)
    []
    (let [[first-range & remaining] ranges
          [merged remaining-unmerged] (merge-overlapping first-range remaining)]
      (cons merged (merge-pass remaining-unmerged)))))

(defn merge-ranges [ranges]
  (loop [current ranges
         previous nil]
    (let [next-merge (merge-pass current)]
      (if (= next-merge previous)
        next-merge
        (recur next-merge current)))))

(->> "resources/5.in"
     parse-input
     first
     merge-ranges
     (map (fn [[start end]] (- (inc end) start)))
     (reduce +))

#programming

Day 4 of solving {{< backlink “aoc” “The Advent of Code” >}} in {{< backlink “clojure” “Clojure” >}}.

The first grid problem of the season! The first part was really suspiciously easy, read in the grid, find all the rolls and look at its 8 neighbors, eliminating it if there are less than 4. The 2nd part was a nice progression on this.

Instead of just saying how many should be eliminated, we do it until there are no eliminations possible. This is highly reminiscent of a Conway Game of Life puzzle. In Clojure this is quite nicely done by reading the grid as a vector, and then ranging over all the coordinates. On each coordinate just take a look at the neighbors and apply the logic. Its pure nature means that the functions are already working in the right way to do this repeatedly.

(ns day4
  (:require
   [clojure.string :as str]))

(defn get-cell [grid [row col]]
  (when (and (>= row 0) (< row (count grid))
             (>= col 0) (< col (count (first grid))))
    (get-in grid [row col])))

(defn neighbors-8 [grid [row col] & {:keys [filter-fn] :or {filter-fn (constantly true)}}]
  (->> (for [r (range (dec row) (+ row 2))
             c (range (dec col) (+ col 2))
             :when (not= [r c] [row col])]
         [r c])
       (map #(get-cell grid %))
       (remove nil?)
       (filter filter-fn)))

(defn all-coords [grid]
  (for [row (range (count grid))
        col (range (count (first grid)))]
    [row col]))

(defn print-grid-with-marks [grid marked-positions]
  (doseq [row (range (count grid))]
    (doseq [col (range (count (first grid)))]
      (let [pos [row col]
            cell (get-in grid pos)]
        (print (if (some #(= % pos) marked-positions)
                 "X "
                 (str cell " ")))))
    (println)))

(defn mark-movable [grid]
  (let [all-pos (all-coords grid)
        marks (remove nil? (map (fn [pos]
                                  (let [n (neighbors-8 grid pos :filter-fn #(not= % \.))]
                                    (when (and (= \@ (get-cell grid pos)) (< (count n) 4))
                                      pos)))
                                all-pos))]
    marks))

(defn remove-marked [grid marked-positions]
  (let [rows (count grid)
        cols (count (first grid))]
    (vec
     (for [row (range rows)]
       (vec
        (for [col (range cols)]
          (let [pos [row col]
                cell (get-in grid pos)]
            (if (and (= cell \@) (some #(= % pos) marked-positions))
              \.
              cell))))))))

(def play
  (fn [grid]
    (loop [g grid
           moved 0]
      (let [marks (mark-movable g)
            mark-count (count marks)]
        (if (= 0 mark-count)
          moved
          (recur (remove-marked g marks)
                 (+ moved mark-count)))))))

(def grid
  (->> "resources/4.in"
       slurp
       str/split-lines
       (mapv vec)))

;; part 1
(count (mark-movable grid))

;; part 2
(play grid)

Really enjoyed it and well within my daily commute to solve.

#programming

Day 3 of solving {{< backlink “aoc” “The Advent of Code” >}} in {{< backlink “clojure” “Clojure” >}}.

The puzzle today has us figuring out the joltage of a bank of batteries. This is a puzzle of the type “largest sequence in a list”. When you start you already know that part 2 will be something that a naive approach will not be able to handle, but I still did part 1 with a naive combination function.

The {{< backlink “clojure” “Clojure” >}} combinatorics library will not work here, as it will take a look at the distinct pairs that will be in the list. So in a list of (2 8 2) it will find the unique pair of (2 8), resulting in the integer 28 for the puzzle, but it will fail to see that 82 was also an option.

I wrote a simple combinations function to start with, and then puzzle 2 hit me with a combination of 12 digits. The algorithm did not work for that amount. I changed it to search for the largest number n positions from the end. So, first search the highest number at least 12 positions away from the end, from then search for the highest number at least 11 positions from the end, etc. The algorithm works beautifully well.

Original: 12397658
First pass (2 positions remaining): ___97658
First pass (1 positions remaining): ___9___8
Result: 98

While writing this I am wondering if it would be even better to convert the string to numbers before finding the digits, but I will leave that as an exercise to you, dear reader.

(ns day3
  (:require
   [clojure.string :as str]))

(defn find-highest-digit
  "Find the highest digit on to the end of the string.
   This is an optimization to not interpolate the entire string."
  [s start end]
  (loop [max-digit nil
         max-pos nil
         pos start]
    (cond
      (> pos end) ;; break out the loop with the current highest number
      {:digit max-digit :pos max-pos}

      (or (nil? max-digit) (> (Character/digit (nth s pos) 10) max-digit))
      (recur (Character/digit (nth s pos) 10) pos (inc pos))

      :else
      (recur max-digit max-pos (inc pos)))))

(defn find-max-number
  "Find the max number of `remaining-length` size. Keep finding
   the max number at least `remaining-length` from the end."
  [s remaining-length]
  (let [n (count s)]
    (when (and (>= n 1) (>= remaining-length 1))
      (let [end-pos (- n remaining-length)
            {:keys [digit pos]} (find-highest-digit s 0 end-pos)]
        (if (nil? digit)
          nil
          (let [remaining-s (if (< (inc pos) n) (subs s (inc pos)) "")]
            (cons digit (find-max-number remaining-s (dec remaining-length)))))))))

(defn max-n-digit-number
  "Find the maximum number in a sequence of number, allowing for
   dropping intermediate numbers"
  [n s]
  (let [result (find-max-number s n)]
    (Long/parseLong (apply str result))))

(->> "resources/3.in"
     slurp
     (str/split-lines)
     (map #(max-n-digit-number 2 %))
     (reduce +))

(->> "resources/3.in"
     slurp
     (str/split-lines)
     (map #(max-n-digit-number 12 %))
     (reduce +))

#programming

Day 2 of solving {{< backlink “aoc” “The Advent of Code” >}} in {{< backlink “clojure” “Clojure” >}}.

The 2nd day had us do some magic with numbers. The quest was to find some silly patterns in the numbers. I chose to use good old regular expressions to do the job, while a colleague of mine chose to use math. Both work really well. The 2nd part of the puzzle is to find repeating patterns, where the 1st only wanted pairs.

(ns day2
  (:require
   [clojure.string :as str]))

(def parse-int Long/parseLong)

(defn parse-ranges [input]
  (map (fn [in]
         (let [[_ start end] (first (re-seq #"(\d+)-(\d+)" in))]
           (filter #(re-matches #"(\d+)\1" (str %))
              (range (parse-int start) (inc (parse-int end))))))
       input))

(defn parse-ranges2 [input]
  (map (fn [in]
         (let [[_ start end] (first (re-seq #"(\d+)-(\d+)" in))]
           (filter #(re-matches #"(\d+)(\1+)" (str %)) 
               (range (parse-int start) (inc (parse-int end))))))
       input))

(def part1 (reduce +
            (->
            (slurp "resources/two.in")
            (str/trim)
            (str/split #",")
            (parse-ranges)
            (flatten))))

(def part2 (reduce + (->
            (slurp "resources/two.in")
            (str/trim)
            (str/split #",")
            (parse-ranges2)
            (flatten))))

(prn part1 part2)

#programming

Day 1 of solving {{< backlink “aoc” “The Advent of Code” >}} in {{< backlink “clojure” “Clojure” >}}.

Each year I like to participate in the Advent of Code. This year there will be 12 puzzles to solve due to Eric taking care of himself, good on you!

My language this year is {{< backlink “clojure” “Clojure” >}}. My solution is quite simple and straightforward. I read the data using split-instructions, transforming the L into a subtraction. For part 1 it is enough to then take the reductions and then filter out all the times we land on the digit 0.

For part 2 I just generate the sequence of numbers for each step in all-steps and then using reduce to apply it to each step, just like the reductions. The result is a long list of numbers from which I just take the 0s again.

(ns one
  (:require
   [clojure.string :as str]))

(defn split-instruction [instruction]
  (let [[_ dir dist] (re-matches #"([A-Z])(\d+)" instruction)
        steps (Integer/parseInt dist)]
    (cond
      (= dir "L") (* steps -1)
      (= dir "R") steps)))

(defn part1 []
  (let [data (->> (slurp "resources/one.in")
                  (str/split-lines)
                  (map split-instruction))
        steps (reductions #(mod (+ %1 %2) 100) 50 data)]
    (count (filter #(= 0 %) steps))))

(part1)

(defn all-steps [start change]
  (let [step      (if (pos? change) 1 -1)
        seq-start (if (pos? change) (inc start) (dec start))
        seq-end   (+ start change step)]
    (map #(mod % 100) (range seq-start seq-end step))))

(defn trace-all-steps [start-pos changes]
  (let [result (reduce (fn [acc change]
                         (let [current-pos (:pos acc)
                               steps (all-steps current-pos change)
                               new-pos (last steps)]
                           {:pos new-pos :history (into (:history acc) steps)}))
                       {:pos start-pos :history []}
                       changes)]
    (:history result)))

(defn part2 []
  (let [data (->> (slurp "resources/one.in")
                  (str/split-lines)
                  (map split-instruction))
        steps (trace-all-steps 50 data)]
    (count (filter #(= 0 %) steps))))

(part2)






#programming

I was under the impression that I was a good typist, but getting an ergonomic keyboard taught me otherwise.

I have long been fascinated by the world of keyboards. I have several mechanical keyboards, most from Ducky. They are wonderful, but they all have in common that they are traditional keyboards. This means that your hands are close together on the keyboard, closing your chest and forcing your hands in an awkward position. We all have become very used to this position, as this is the way typewriters taught us all how to type.

Then I found an Ergodox EZ on Marktplaats, a website where consumers can offer goods for sale. The price was still hefty, but not the 350+ euros it is brand new. A few days later it arrived, turns out it was indeed hardly used. It has started my journey to actually learn how to type again.

In the 30 years I have been in the software industry I learned to type quite fast, but putting my hands apart, each hand responsible for their own keys, turns out to be quite a challenge. There is even a blogpost on the topic by the creators of the keyboard. You see, it is not just the hands apart that gets you, it is the columnar key-setting. This means that the keys are not only separated but instead of staggered as on a normal keyboard, they are in a line above each-other.

The other thing about the keyboard is that you can customize pretty much everything. It has only 4 rows of keys, so many features of a normal keyboard hide in different layers on the keyboard. As I do a lot of coding and technical writing, I have different needs from someone who does novel writing. This is a great personalization of the keyboard for me, it truly represents me and the work I do. This is my current layout, notice the brackets on the side, and the access to CTRL and ALT in the middle. This was taken from connecting it on a mac.

My keyboard layout

I now have a little over a week with this layout, and it seems to work very nicely for me. My words-per-minute are going up again and it feels natural. Soon I will need to get a carry case for my keyboard as normal keyboards are starting to feel alien to me.

#programming

When I tell people that I like to code in {{< backlink “clojure” “Clojure”>}} the common response is “wut?”. Clojure is not known as a programming language in which you create big systems. As all Clojure people know, this is not true. There are many systems written in Clojure. Let me show you some that are very actively maintained.

First there is Lipas, a Finnish platform that shows you information about sports clubs. The structure and techniques used in this code base I use as a reference implementation for my own ClojureScript + Clojure systems. A screenshot of the application is shown here:

Lipas

Next, there is Metabase, a business intelligence platform. The below gif shows you some of the features it has.

Metabase

There is a great talk at Conj 2024 about supporting 50000 users on Metabase. You can watch it over on YouTube.

Finally, also found on the Conj 2024 streams, there is Cisco Threat Intelligence API. This a full threat intelligence service and data model that is built using Clojure. Link to the repository. The talk about the project can be seen on YouTube.

There are plenty of other projects using Clojure, if you know of more that I should add to my list, do let me know!

#clojure #web #programming

Observability in cloud-native applications is crucial for managing complex systems and ensuring reliability (Chakraborty & Kundan, 2021; Kosińska et al., 2023). It enables continuous generation of actionable insights based on system signals, helping teams deliver excellent customer experiences despite underlying complexities (Hausenblas, 2023; Chakraborty & Kundan, 2021). In essence, adding proper observability to your system allows you to find and diagnose issues without having to dig through tons of unstructured log files.

The running project

In {{< backlink “20250107-clojure-reitit-server” “my previous post on reitit”>}} we built a simple endpoint using {{< backlink “clojure” “Clojure”>}} and reitit. The complete code for the small project was:

(ns core
  (:require
   [reitit.ring :as ring]
   [ring.adapter.jetty :as jetty]))

(defn handler [request]
  {:status 200
   :body (str "Hello world!")})

(def router (ring/router
             ["/hello" {:get #'handler}]))

(def app (ring/ring-handler router
                            (ring/create-default-handler)))

Nice and easy eh? That simplicity is what I truly love about {{< backlink “clojure” “Clojure”>}}. That, and the fact that there is an awesome interoperability with the Java ecosystem of libraries.

Adding observability

In {{< backlink “clojure” “Clojure”>}} it is possible to add observability through the wonderful clj-otel library by Steffan Westcott. It implements the OpenTelemetry standard which makes it integrate nicely in products such as HoneyComb.io and Jaeger.

The library has a great tutorial that you can follow here. Applying the knowledge from this tutorial to our reitit application is also trivial. To show the power of observability a JDBC connection will be added to the application. It is not necessary to mess with any tables or such, it will just leverage a connection to a Postgres database and a value will be queried from it.

First, lets see the updated deps.edn file.

{:deps {ring/ring-jetty-adapter {:mvn/version "1.13.0"}
        metosin/reitit {:mvn/version "0.7.2"}

        ;; Observability
        com.github.steffan-westcott/clj-otel-api {:mvn/version "0.2.7"}
        
        ;; Database access
        com.github.seancorfield/next.jdbc {:mvn/version "1.3.981"}
        org.postgresql/postgresql {:mvn/version "42.7.4"}
        com.zaxxer/HikariCP {:mvn/version "6.2.1"}}

 :aliases {:otel {:jvm-opts ["-javaagent:opentelemetry-javaagent.jar"
                             "-Dotel.resource.attributes=service.name=blog-service"
                             "-Dotel.metrics.exporter=none"
                             ]}}}

You will notice some new dependencies, as well as an alias that you can use to start the repl with. If you, like me, use Emacs you can codify this into a .dir-locals.el file for your project.

((nil . ((cider-clojure-cli-aliases . ":otel"))))

Now, whenever cider creates a new repl it will use the otel alias as well.

The agent that is listed as javaagent can be downloaded from the OpenTelemetry Java Instrumentation page. This will immediately bring in a slew of default instrumentations to the project. Give it a try with the starter project, you will notice that all the jetty requests will show up in your jaeger instance (you did look at the tutorial, right?).

Finally, here is the update project for you to play with.

(ns core
  (:require
   [next.jdbc :as jdbc]
   [reitit.ring :as ring]
   [ring.adapter.jetty :as jetty]
   [ring.util.response :as response]
   [steffan-westcott.clj-otel.api.trace.http :as trace-http]
   [steffan-westcott.clj-otel.api.trace.span :as span]))

(def counter (atom 0))

;; add your database configuration here
(def db {:jdbcUrl "jdbc:postgresql://localhost:5432/db-name?user=db-user&password=db-pass"})

(def ds (jdbc/get-datasource db))

(defn wrap-db
  [handler db]
  (fn [req]
    (handler (assoc req :db db))))

(defn wrap-exception [handler]
  (fn [request]
    (try
      (handler request)
      (catch Throwable e
        (span/add-exception! e {:escaping? false})
        (let [resp (response/response (ex-message e))]
          (response/status resp 500))))))

(defn db->value [db]
  (let [current @counter]
    (span/with-span! "Incrementing counter"
      (span/add-span-data! {:attributes {:service.counter/count current}})
      (swap! counter inc))
    (:value (first (jdbc/execute! db [(str "select " current " as value")])))))

(defn handler [request]
  (let [db (:db request)
        dbval (db->value db)]
    (span/add-span-data! {:attributes {:service.counter/count dbval}})
    {:status 200
     :body (str "Hello world: " dbval)}))

(def router (ring/router
             ["/hello" {:get (-> #'handler
                                 (wrap-db ds)
                                 wrap-exception
                                 trace-http/wrap-server-span)}]))
                                 
(def app (ring/ring-handler router
                            (ring/create-default-handler)))

(def server (jetty/run-jetty #'app {:port 3000, :join? false}))
;; (.stop server)

There are several interesting bits to be aware of. First the handler is wrapped in several middleware functions, one to pass the database connection, the other to wrap the exceptions (such as in the tutorial) and finally the middleware to wrap a server request. The db->value creates its own span to keep track of its activity.

After making several requests you will see that Jaeger contains the same amount of traces. A normal trace will show 3 bars, each of which you can expand and explore.

A trace in Jaeger

If you take the database offline (that is why we used Postgres), you will notice that the exception is neatly logged.

Exceptions in Jaeger

Observability allows you to get a great insight into how you application is running in production. With the clj-otel library it is a breeze to enhance your own application.

#clojure #web #observability #programming

{{}} Currently, only use Postgres 14 on the Digital Ocean application platform for development databases. {{}}

While following the book {{< backlink “zero2prod” “Zero2Prod”>}} you will learn how to deploy a {{< backlink “rust” “Rust”>}} application to digital ocean through a Continuous Deployment pipeline. This is hardly anything new for me, I even teach a course in DevOps, but to not stray from the path of the book I followed its instructions.

The spec for digital ocean looks like this (this is abbreviated for your reading pleasure):

name: zero2prod
region: fra
services:
    - name: zero2prod
      dockerfile_path: Dockerfile
      source_dir: .
      github:
        branch: main
        deploy_on_push: true
        repo: credmp/zero2prod
      health_check:
        http_path: /health_check
      http_port: 8000
      instance_count: 1
      instance_size_slug: basic-xxs
      routes:
      - path: /
databases:
  - name: newsletter
    engine: PG
    db_name: newsletter
    db_user: newsletter
    num_nodes: 1
    size: db-s-dev-database
    version: "16"

Actually, in the book it says to use version 12, but that version is no longer available. The latest version support is 16 and I chose that. There is only a small hiccup here, since Postgres 15 in 2022 there has been a breaking change in how databases are created. Notable, a best practice following a CVE in 2018 (CVE-2018-1058), has been made the standard. The standard being that by default users do not have creation rights, as an administrator you have to explicitly grant rights to your users.

Although this has been best practice since 2018, the change in Postgres 15 confronts users with this change. To my surprise Digital Ocean seems to not be aware of this change until now.

The development database created in the application platform using the spec from above creates an user (newsletter) with the following rights:

Role name | Attributes
------------------+------------------------------------------------------------
_doadmin_managed | Cannot login
_doadmin_monitor |
_dodb | Superuser, Replication
doadmin | Create role, Create DB, Replication, Bypass RLS
doadmin_group | Cannot login
newsletter |
postgres | Superuser, Create role, Create DB, Replication, Bypass RLS

You read that correctly, none. At the moment you can still create a postgres 14 database with digital ocean, which grants rights to the user and then you can upgrade it to the latest version, keeping the rights. But that is a workaround.

After determining the cause of the error I decided to mail digital ocean support with the issue. Timeline:

  • December 30th: the answer is that I am using a development database, if I would only upgrade to a managed cluster I would have full access to the database. I politely responded explaining the problem again.
  • December 30th: a quick response from the same agent, saying that based on the information provided I am trying to do things with the doadmin user, again not reading the actual question (or not understanding the problem). I again answer with a full log of the creation of the database and the rights given to the users.
  • December 31st: another agent responds, telling me that using my spec it will create a database and that I can connect using the data from the control panel. This is exactly the information I already sent, but the agent does not actually look at the problem (no rights). I once again explain the issue.
  • December 31st: another agent answers the ticket, asking how I create the database. I once again answer with the spec (which is already in the ticket 2 times now) and the steps I use (doctl from the command line).
  • December 31st: another agent responds with some general information about creating databases, again not actually reading or understanding the issue.
  • Januari 1st: a standard follow up email asking if I am happy with the service. I respond that the problem is not solved, and that I am fearful that given the interaction it will not be solved.
  • Januari 2nd: another agent responds that they are talking internally
  • Januari 2nd: a senior agent called Nate appears in the thread. Actually asking questions that explore the issue. I promptly respond.
  • Januari 2nd: Nate acknowledges the issues and Digital Ocean starts working on a fix for their database provisioning. Provides the workaround of first using version 13 or 14 and then upgrading.
  • Januari 9th: Still working
  • Januari 15th: Still working
  • Januari 21st: Another update that the provisioning process is quite complex and they are still working on a solution.

The proces to get something so trivial through the support channel is quite painful. I do realize I do not have paid support, and I am willing to wait it out because of that, but the first 5 interactions did nothing but destroy my confidence in the Digital Ocean support system. Luckily Nate picked up the ticket.

When a solution eventually comes around I will update this post.

#development #database #programming