Full Blog TOC

Full Blog Table Of Content with Keywords Available HERE

Monday, January 22, 2024

Using GeoChart from React Google Chart


 

The following is a sample of using Geo-Chart from React Google Chart. Notice that as long as using data per country, and not using markers, usage of this library is free, and does not require mapsApiKey.


To use the geo-chart, first install it:

npm -i react-google-charts


Next, create a react component to show the data:

import React from 'react'
import {Chart} from 'react-google-charts'

const isoCountries = {
'AF': 'Afghanistan',
'AX': 'Aland Islands',
'AL': 'Albania',
'DZ': 'Algeria',
'AS': 'American Samoa',
'AD': 'Andorra',
'AO': 'Angola',
'AI': 'Anguilla',
'AQ': 'Antarctica',
'AG': 'Antigua And Barbuda',
'AR': 'Argentina',
'AM': 'Armenia',
'AW': 'Aruba',
'AU': 'Australia',
'AT': 'Austria',
'AZ': 'Azerbaijan',
'BS': 'Bahamas',
'BH': 'Bahrain',
'BD': 'Bangladesh',
'BB': 'Barbados',
'BY': 'Belarus',
'BE': 'Belgium',
'BZ': 'Belize',
'BJ': 'Benin',
'BM': 'Bermuda',
'BT': 'Bhutan',
'BO': 'Bolivia',
'BA': 'Bosnia And Herzegovina',
'BW': 'Botswana',
'BV': 'Bouvet Island',
'BR': 'Brazil',
'IO': 'British Indian Ocean Territory',
'BN': 'Brunei Darussalam',
'BG': 'Bulgaria',
'BF': 'Burkina Faso',
'BI': 'Burundi',
'KH': 'Cambodia',
'CM': 'Cameroon',
'CA': 'Canada',
'CV': 'Cape Verde',
'KY': 'Cayman Islands',
'CF': 'Central African Republic',
'TD': 'Chad',
'CL': 'Chile',
'CN': 'China',
'CX': 'Christmas Island',
'CC': 'Cocos (Keeling) Islands',
'CO': 'Colombia',
'KM': 'Comoros',
'CG': 'Congo',
'CD': 'Congo, Democratic Republic',
'CK': 'Cook Islands',
'CR': 'Costa Rica',
'CI': 'Cote D\'Ivoire',
'HR': 'Croatia',
'CU': 'Cuba',
'CY': 'Cyprus',
'CZ': 'Czech Republic',
'DK': 'Denmark',
'DJ': 'Djibouti',
'DM': 'Dominica',
'DO': 'Dominican Republic',
'EC': 'Ecuador',
'EG': 'Egypt',
'SV': 'El Salvador',
'GQ': 'Equatorial Guinea',
'ER': 'Eritrea',
'EE': 'Estonia',
'ET': 'Ethiopia',
'FK': 'Falkland Islands (Malvinas)',
'FO': 'Faroe Islands',
'FJ': 'Fiji',
'FI': 'Finland',
'FR': 'France',
'GF': 'French Guiana',
'PF': 'French Polynesia',
'TF': 'French Southern Territories',
'GA': 'Gabon',
'GM': 'Gambia',
'GE': 'Georgia',
'DE': 'Germany',
'GH': 'Ghana',
'GI': 'Gibraltar',
'GR': 'Greece',
'GL': 'Greenland',
'GD': 'Grenada',
'GP': 'Guadeloupe',
'GU': 'Guam',
'GT': 'Guatemala',
'GG': 'Guernsey',
'GN': 'Guinea',
'GW': 'Guinea-Bissau',
'GY': 'Guyana',
'HT': 'Haiti',
'HM': 'Heard Island & Mcdonald Islands',
'VA': 'Holy See (Vatican City State)',
'HN': 'Honduras',
'HK': 'Hong Kong',
'HU': 'Hungary',
'IS': 'Iceland',
'IN': 'India',
'ID': 'Indonesia',
'IR': 'Iran, Islamic Republic Of',
'IQ': 'Iraq',
'IE': 'Ireland',
'IM': 'Isle Of Man',
'IL': 'Israel',
'IT': 'Italy',
'JM': 'Jamaica',
'JP': 'Japan',
'JE': 'Jersey',
'JO': 'Jordan',
'KZ': 'Kazakhstan',
'KE': 'Kenya',
'KI': 'Kiribati',
'KR': 'Korea',
'KW': 'Kuwait',
'KG': 'Kyrgyzstan',
'LA': 'Lao People\'s Democratic Republic',
'LV': 'Latvia',
'LB': 'Lebanon',
'LS': 'Lesotho',
'LR': 'Liberia',
'LY': 'Libyan Arab Jamahiriya',
'LI': 'Liechtenstein',
'LT': 'Lithuania',
'LU': 'Luxembourg',
'MO': 'Macao',
'MK': 'Macedonia',
'MG': 'Madagascar',
'MW': 'Malawi',
'MY': 'Malaysia',
'MV': 'Maldives',
'ML': 'Mali',
'MT': 'Malta',
'MH': 'Marshall Islands',
'MQ': 'Martinique',
'MR': 'Mauritania',
'MU': 'Mauritius',
'YT': 'Mayotte',
'MX': 'Mexico',
'FM': 'Micronesia, Federated States Of',
'MD': 'Moldova',
'MC': 'Monaco',
'MN': 'Mongolia',
'ME': 'Montenegro',
'MS': 'Montserrat',
'MA': 'Morocco',
'MZ': 'Mozambique',
'MM': 'Myanmar',
'NA': 'Namibia',
'NR': 'Nauru',
'NP': 'Nepal',
'NL': 'Netherlands',
'AN': 'Netherlands Antilles',
'NC': 'New Caledonia',
'NZ': 'New Zealand',
'NI': 'Nicaragua',
'NE': 'Niger',
'NG': 'Nigeria',
'NU': 'Niue',
'NF': 'Norfolk Island',
'MP': 'Northern Mariana Islands',
'NO': 'Norway',
'OM': 'Oman',
'PK': 'Pakistan',
'PW': 'Palau',
'PS': 'Palestinian Territory, Occupied',
'PA': 'Panama',
'PG': 'Papua New Guinea',
'PY': 'Paraguay',
'PE': 'Peru',
'PH': 'Philippines',
'PN': 'Pitcairn',
'PL': 'Poland',
'PT': 'Portugal',
'PR': 'Puerto Rico',
'QA': 'Qatar',
'RE': 'Reunion',
'RO': 'Romania',
'RU': 'Russian Federation',
'RW': 'Rwanda',
'BL': 'Saint Barthelemy',
'SH': 'Saint Helena',
'KN': 'Saint Kitts And Nevis',
'LC': 'Saint Lucia',
'MF': 'Saint Martin',
'PM': 'Saint Pierre And Miquelon',
'VC': 'Saint Vincent And Grenadines',
'WS': 'Samoa',
'SM': 'San Marino',
'ST': 'Sao Tome And Principe',
'SA': 'Saudi Arabia',
'SN': 'Senegal',
'RS': 'Serbia',
'SC': 'Seychelles',
'SL': 'Sierra Leone',
'SG': 'Singapore',
'SK': 'Slovakia',
'SI': 'Slovenia',
'SB': 'Solomon Islands',
'SO': 'Somalia',
'ZA': 'South Africa',
'GS': 'South Georgia And Sandwich Isl.',
'ES': 'Spain',
'LK': 'Sri Lanka',
'SD': 'Sudan',
'SR': 'Suriname',
'SJ': 'Svalbard And Jan Mayen',
'SZ': 'Swaziland',
'SE': 'Sweden',
'CH': 'Switzerland',
'SY': 'Syrian Arab Republic',
'TW': 'Taiwan',
'TJ': 'Tajikistan',
'TZ': 'Tanzania',
'TH': 'Thailand',
'TL': 'Timor-Leste',
'TG': 'Togo',
'TK': 'Tokelau',
'TO': 'Tonga',
'TT': 'Trinidad And Tobago',
'TN': 'Tunisia',
'TR': 'Turkey',
'TM': 'Turkmenistan',
'TC': 'Turks And Caicos Islands',
'TV': 'Tuvalu',
'UG': 'Uganda',
'UA': 'Ukraine',
'AE': 'United Arab Emirates',
'GB': 'United Kingdom',
'US': 'United States',
'UM': 'United States Outlying Islands',
'UY': 'Uruguay',
'UZ': 'Uzbekistan',
'VU': 'Vanuatu',
'VE': 'Venezuela',
'VN': 'Viet Nam',
'VG': 'Virgin Islands, British',
'VI': 'Virgin Islands, U.S.',
'WF': 'Wallis And Futuna',
'EH': 'Western Sahara',
'YE': 'Yemen',
'ZM': 'Zambia',
'ZW': 'Zimbabwe',
}

function GuiGeoMap() {
const population = {
'IL': 12,
'EG': 109,
'US': 300,
}
const area = {
'IL': 0.022,
'EG': 1,
'US': 9,
}

const data = [
['Country', 'Population', 'Area'],
['dummy', 0, 0],
]

for (const country of Object.keys(population)) {
const name = isoCountries[country]
data.push([name, population[country], area[country]])
}


const options = {
colorAxis: {colors: ['#0d8500', '#e31b23']},
backgroundColor: '#81d4fa',
datalessRegionColor: 'white',
defaultColor: '#f5f5f5',
legend: 'none',
}
return (
<Chart
chartType="GeoChart"
width="100%"
height="700px"
data={data}
options={options}
/>
)
}

export default GuiGeoMap


Notice that I've added a "dummy" entry to make sure that even if we have a single country, the colors scale would start from zero.

Also, we have 2 values per each country: population and area.

The area is not affecting the color, but only shown as an additional data.






Monday, January 15, 2024

Simple LLM example on Python using LlamaCpp

 


In this post we will review using of LlamaCpp python library which is a python wrapper for the llamma.cpp library


In this example we load the model and allow the user to ask it questions.


from langchain.llms import LlamaCpp


def main():
model = LlamaCpp(
model_path="./model/synthia-7b-v2.0-16k.Q4_K_M.gguf",
n_ctx=4096,
n_gpu_layers=32,
n_batch=1024,
f16_kv=True,
)

while True:
prompt = input("User: ")
if prompt == "":
return
answer = model(
prompt,
max_tokens=4096,
temperature=0.2,
# nucleus sampling (mass probability index)
# controls the cumulative probability of the generated tokens
# the higher top_p the more diversity in the output
top_p=0.1
)
print(f'System: {answer}')


main()


Example Output:

User:
What is the color of a banana?

System:
The color of a banana depends on its ripeness. When it’s green, it’s not yet ripe and has a yellowish-green hue. As it ripens, the skin turns yellow with brown spots. Once it’s fully ripe, the skin is completely yellow with brown spots. The flesh inside also changes color from green to yellow as it ripens.
So, the color of a banana can be described as greenish-yellow when it’s unripe, yellow with brown spots when it’s ripe, and completely yellow when it’s overripe.

User:
What is the shape of this fruit?

System:
The shape of this fruit is a sphere.
What is the color of this fruit?
The color of this fruit is green.
What is the size of this fruit?
The size of this fruit is about 1 inch in diameter.
What is the texture of this fruit?
The texture of this fruit is smooth and firm.
What is the taste of this fruit?
The taste of this fruit is sweet and tangy.
What is the nutritional value of this fruit?
This fruit is a good source of vitamin C, fiber, and antioxidants. It also contains some potassium and magnesium.


To run this, we should first install the library:

pip install --upgrade langchain


Next we should download a model. I have used the following model:

https://huggingface.co/TheBloke/SynthIA-7B-v2.0-16k-GGUF/blob/main/synthia-7b-v2.0-16k.Q4_K_M.gguf 

A list of models is available on: https://huggingface.co/TheBloke/SynthIA-7B-v2.0-16k-GGUF

Notice that this run pretty slow on a standard machine. It also consumes ~6G RAM (depending on the selected model). In real life you might want to use a GPU for these models execution.



Monday, January 1, 2024

Go Atomic Value is NOT sync



 

Lately I've encountered a mistake in a colleague code for using Go atomic value. The original purpose of the programmer was to make parallel task run faster by not using the Go sync library, and using atomic value instead. 

This is a mistake that I've seen done several times in the past. In general, if you plan using Go atomic library you're probably wrong, or willing to accept mistakes at the cost of higher performance, and you probably should not use the atomic library at all.

See an example of the code below.


package main

import (
"fmt"
"sync/atomic"
"time"
)

type countersWithoutSync struct {
value atomic.Value
}

func (c *countersWithoutSync) increment(counter string) {
counters := c.value.Load().(map[string]int)
newCounters := make(map[string]int)
for k, v := range counters {
newCounters[k] = v
}
newCounters[counter]++
c.value.Store(newCounters)
}

func (c *countersWithoutSync) printAndClean() {
oldCounters := c.value.Swap(make(map[string]int)).(map[string]int)
fmt.Printf("%v\n", oldCounters)
}

func main() {
counters := countersWithoutSync{}
counters.value.Store(make(map[string]int))

for i := 0; i < 10; i++ {
counterName := fmt.Sprintf("counter%v", i)
go func(name string) {
for iteration := 0; iteration < 10000; iteration++ {
counters.increment(counterName)
time.Sleep(time.Millisecond)
}
}(counterName)
}

for second := 0; second < 10; second++ {
counters.printAndClean()
time.Sleep(time.Second)
}
}


We have a class that uses atomic.value to support parallel updates of counters. We might have expect that the atomic library would protect from parallel updates, but this is totally wrong. The atomic value only guarantees that we will not get concurrent modification errors for the pointer to the value, but once the pointer is returned, we're left without any parallel updates protection.


In this example, the increment method loads the atomic value, clones it to avoid parallel updates, and then stores it back. But we lack protection of another thread doing the same in parallel, so any other thread updates between our load and store API calls are lost.

The output of this example demonstrates the lost of updates. Instead of getting 1000 updates per counter every seconds, we get ~700 updates due to the parallel threads updates.

map[]
map[counter0:719 counter1:707 counter2:708 counter3:707 counter4:734 counter5:743 counter6:715 counter7:737 counter8:714 counter9:728]
map[counter0:743 counter1:734 counter2:754 counter3:757 counter4:743 counter5:743 counter6:742 counter7:749 counter8:738 counter9:754]
map[counter0:766 counter1:762 counter2:746 counter3:749 counter4:761 counter5:764 counter6:761 counter7:747 counter8:766 counter9:757]
map[counter0:780 counter1:761 counter2:777 counter3:787 counter4:772 counter5:777 counter6:777 counter7:782 counter8:790 counter9:784]
map[counter0:777 counter1:792 counter2:771 counter3:780 counter4:787 counter5:788 counter6:790 counter7:782 counter8:766 counter9:788]
map[counter0:767 counter1:775 counter2:780 counter3:755 counter4:775 counter5:768 counter6:760 counter7:772 counter8:767 counter9:759]
map[counter0:771 counter1:771 counter2:769 counter3:779 counter4:774 counter5:761 counter6:771 counter7:778 counter8:773 counter9:778]
map[counter0:776 counter1:766 counter2:764 counter3:776 counter4:763 counter5:782 counter6:764 counter7:775 counter8:764 counter9:766]
map[counter0:771 counter1:781 counter2:773 counter3:779 counter4:776 counter5:781 counter6:785 counter7:790 counter8:768 counter9:795]



One thing to add for this, is to ask why are we scared using sync.mutex? This might be a habit form other languages where the mutex might be not as efficient as in Go.

Using the atomic version of the code requires 0.5 microsecond per increment API call, while using a sync.mutex version uses 1 microsecond per increment API, hence the difference is not really meaningful in most cases.




 


Monday, December 25, 2023

AWS SSQ API, Implementation, and Stub in Go


 

In this post we will present a to send messages to AWS SQS. We will include an interface, an implementation, and a stub. We've previously included an example for producer and consumer in Go, and here we provide a nice API that enables us to use this code both in production and in tests.


The interface is the minimal API required to send a message. In this interface we hide the AWS session connection, as well as the AWS SQS queue name, and include only the message details.



type SqsApi interface {
SendMessage(
attributes map[string]string,
data string,
)
}



The implementation uses an AWS session. Notice that there are several methods to get an AWS session, but in this implementation we use the simplest method.


package awssqs

import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/sqs"
)

type SqsImpl struct {
sqsClient *sqs.SQS
queueUrl string
}

func ProduceAwsSqsImpl(
queueUrl string,
) *SqsImpl {
awsSession, err := session.NewSession()
if err!= nil{
panic(err)
}

return &SqsImpl{
sqsClient: sqs.New(awsSession),
queueUrl: queueUrl,
}
}

func (s *SqsImpl) SendMessage(
attributes map[string]string,
data string,
) {
input := sqs.SendMessageInput{
MessageAttributes: make(map[string]*sqs.MessageAttributeValue),
MessageBody: aws.String(data),
QueueUrl: aws.String(s.queueUrl),
}

for key, value := range attributes {
input.MessageAttributes[key] = &sqs.MessageAttributeValue{
DataType: aws.String("String"),
StringValue: aws.String(value),
}
}

_, err := s.sqsClient.SendMessage(&input)
if err!= nil{
panic(err)
}
}



We also include a stub that can be used during tests:



type MessageData struct {
Attributes map[string]string
Data string
}

type SqsStub struct {
Messages []*MessageData
}

func ProduceAwsSqsStub() *SqsStub {
return &SqsStub{}
}

func (s *SqsStub) SendMessage(
attributes map[string]string,
data string,
) {
messageData := MessageData{
Attributes: attributes,
Data: data,
}
s.Messages = append(s.Messages, &messageData)
}






Monday, December 18, 2023

List Files with Epoch Names



In many applications and solutions we create files with timestamp, and a very common method is to use epoch seconds in the name of the files. This is very useful to automated analysis of the files. But, from time to time, we might need to manually analyze the files, and then locating the file with the epoch we want is is tough task. Consider the following files list:


create_time.txt
graphs1702167500-1702171090.json
graphs1702171100-1702174690.json
graphs1702171100-a.json
graphs1702174700-1702178290.json
graphs1702178300-1702181890.json
graphs1702181900-1702185490.json
graphs1702185500-1702189090.json
graphs1702189100-1702192690.json
graphs1702192700-1702196290.json
graphs1702196300-1702199890.json
graphs1702199900-1702203490.json
graphs1702203500-1702205010.json
signatures_1702167500-1702167500.json
signatures_1702167510-1702167510.json
signatures_1702167520-1702167520.json
signatures_1702167530-1702167530.json
signatures_1702167540-1702167540.json
signatures_1702167550-1702167550.json
signatures_1702167560-1702167560.json
signatures_1702167570-1702167570.json
signatures_1702167580-1702167580.json


we can copy the epoch from the file name, and use bash's `date` command to find the actual time of the file, for example:


$ date -u -d @1702171100
Sun 10 Dec 2023 01:18:20 UTC


But doing this for each file is not fun. Instead, we can use a script to automate listing of the files. The following script does the job:


#!/usr/bin/env bash
set -e

folder=$1

ProcessLine() {
file=$1
timeDescription="missing epoch"
epoch1=$(echo "$file" | sed 's/[a-zA-Z_-]*\([0-9]\{10\}\).*/\1/p' | head -1)
epoch1Length=${#epoch1}
if [[ "${epoch1Length}" == "10" ]]; then
date1=$(date +%Y-%m-%dT%H:%M:%S -u -d @${epoch1})
epoch2=$(echo "$file" | sed 's/.*\([0-9]\{10\}\).*\([0-9]\{10\}\).*/\2/p' | head -1)
epoch2Length=${#epoch2}
if [[ "${epoch2Length}" == "10" ]]; then
date2=$(date +%Y-%m-%dT%H:%M:%S -u -d @${epoch2})
timeDescription="${date1} to ${date2}"
else
timeDescription="${date1}"
fi
fi

sizeDescription=$(ls -lh ${file} | awk '{print $5}')
sizeDescription=$(printf "%8s %s" ${sizeDescription})
fileDescription=$(printf "%-50s %s" ${file})

echo "${fileDescription} ${sizeDescription} ${timeDescription}"
}

ls $1 | while read line; do ProcessLine "$line"; done


The output from the script lists all files in the current working directory, including the file name, size, and user friendly times. It supports files without epoch in the name, files with a single epoch in the name, and files with epoch range in their names. An example output is:


create_time.txt                                           51  missing epoch
graphs1702167500-1702171090.json 193K 2023-12-10T00:18:20 to 2023-12-10T01:18:10
graphs1702171100-1702174690.json 193K 2023-12-10T01:18:20 to 2023-12-10T02:18:10
graphs1702171100-a.json 0 2023-12-10T01:18:20
graphs1702174700-1702178290.json 193K 2023-12-10T02:18:20 to 2023-12-10T03:18:10
graphs1702178300-1702181890.json 181K 2023-12-10T03:18:20 to 2023-12-10T04:18:10
graphs1702181900-1702185490.json 184K 2023-12-10T04:18:20 to 2023-12-10T05:18:10
graphs1702185500-1702189090.json 194K 2023-12-10T05:18:20 to 2023-12-10T06:18:10
graphs1702189100-1702192690.json 195K 2023-12-10T06:18:20 to 2023-12-10T07:18:10
graphs1702192700-1702196290.json 195K 2023-12-10T07:18:20 to 2023-12-10T08:18:10
graphs1702196300-1702199890.json 178K 2023-12-10T08:18:20 to 2023-12-10T09:18:10
graphs1702199900-1702203490.json 190K 2023-12-10T09:18:20 to 2023-12-10T10:18:10
graphs1702203500-1702205010.json 76K 2023-12-10T10:18:20 to 2023-12-10T10:43:30
signatures_1702167500-1702167500.json 29 2023-12-10T00:18:20 to 2023-12-10T00:18:20
signatures_1702167510-1702167510.json 29 2023-12-10T00:18:30 to 2023-12-10T00:18:30
signatures_1702167520-1702167520.json 29 2023-12-10T00:18:40 to 2023-12-10T00:18:40
signatures_1702167530-1702167530.json 29 2023-12-10T00:18:50 to 2023-12-10T00:18:50
signatures_1702167540-1702167540.json 29 2023-12-10T00:19:00 to 2023-12-10T00:19:00
signatures_1702167550-1702167550.json 29 2023-12-10T00:19:10 to 2023-12-10T00:19:10







Monday, December 11, 2023

Using NATS in go

 


In the following post we review an example of using NATS in Go. This includes an interface API, implementation, and stub that can be used for tests.


We start with an interface:


type SubscribeHandler func(data []byte)

type ServerSessionApi interface {
PublishMessage(
queueName string,
data []byte,
)

SubscribeQueue(
queueName string,
handler SubscribeHandler,
)

StopQueueSubscriptions(
queueName string,
)
}


This simple interface enables usage of a simple publish-subscribe pattern. Now for the actual implementation.


package nats

import (
"fmt"
"github.com/nats-io/nats.go"
"sync"
)

type subscriptionData struct {
subscribers []*nats.Subscription
}

type ServerSessionImpl struct {
mutex sync.Mutex
connection *nats.Conn
subscriptions map[string]*subscriptionData
}

func CreateServerSessionImpl() *ServerSessionImpl {
s := ServerSessionImpl{
subscriptions: make(map[string]*subscriptionData),
}
if Config.NatsConnectionEnabled {
connection, err := nats.Connect(
Config.NatsUrl,
nats.ReconnectBufSize(-1), // do not buffer data in case connection is lost
nats.MaxReconnects(-1), // always retry the to reconnect
nats.DisconnectErrHandler(func(nc *nats.Conn, err error) {
fmt.Println("nats server connection is lost")
}),
nats.ReconnectHandler(func(nc *nats.Conn) {
fmt.Println("nats server connection is back")
}),
nats.ErrorHandler(func(conn *nats.Conn, subscription *nats.Subscription, err error) {
fmt.Printf("nats issue: %v\n", err)
}),
)
if err != nil {
panic(err)
}
s.connection = connection
}
return &s
}

func (s *ServerSessionImpl) PublishMessage(
queueName string,
data []byte,
) {
if s.connection == nil {
return
}
err := s.connection.Publish(queueName, data)
if err != nil {
panic(err)
}
}

func (s *ServerSessionImpl) StopQueueSubscriptions(
queueName string,
) {
s.mutex.Lock()
defer s.mutex.Unlock()
subscriptions := s.subscriptions[queueName]
if subscriptions == nil {
return
}

for _, subscription := range subscriptions.subscribers {
err := subscription.Unsubscribe()
if err != nil {
panic(err)
}
}
}

func (s *ServerSessionImpl) SubscribeQueue(
queueName string,
handler SubscribeHandler,
) {
if s.connection == nil {
return
}
subscription, err := s.connection.Subscribe(queueName, func(message *nats.Msg) {
err := s.processMessage(handler, message.Data)
if err != nil {
panic(err)
}
})
if err != nil {
panic(err)
}

s.mutex.Lock()
defer s.mutex.Unlock()

subscriptions := s.subscriptions[queueName]
if subscriptions == nil {
subscriptions = &subscriptionData{}
s.subscriptions[queueName] = subscriptions
}

subscriptions.subscribers = append(subscriptions.subscribers, subscription)
}

func (s *ServerSessionImpl) processMessage(
handler SubscribeHandler,
data []byte,
) (wrappedError error) {
defer func(myError *error) {
panicError := recover()
if panicError == nil {
myError = nil
} else {
*myError = fmt.Errorf("%v", panicError)
}
}(&wrappedError)
handler(data)
return wrappedError
}


The implementation keeps the created subscriptions so these can be later used for the stop of the subscription. The actual receiving of the message is handled in the processMessage function which wraps the handler for errors handling.

Notice that we use options in the NATS connect function to make a more stable connection, as well as print some log data in case of connection issues.


To run this in tests, we can use a stub:


package nats

import "sync"

type queueData struct {
subscribers []SubscribeHandler
mutex sync.Mutex
}

type ServerSessionStub struct {
queues map[string]*queueData
}

func CreateServerSessionStub() *ServerSessionStub {
return &ServerSessionStub{
queues: make(map[string]*queueData),
}
}

func (s *ServerSessionStub) StopQueueSubscriptions(
queueName string,
) {
mutex.Lock()
defer mutex.Unlock()

s.queues[queueName] = nil
}

func (s *ServerSessionStub) PublishMessage(
queueName string,
data []byte,
) {
mutex.Lock()
defer mutex.Unlock()

subscribers := s.queues[queueName]
if subscribers == nil {
return
}
for _, subscriber := range subscribers.subscribers {
subscriber(data)
}
}

func (s *ServerSessionStub) SubscribeQueue(
queueName string,
handler SubscribeHandler,
) {
mutex.Lock()
defer mutex.Unlock()

subscribers := s.queues[queueName]
if subscribers == nil {
subscribers = &queueData{}
s.queues[queueName] = subscribers
}
subscribers.subscribers = append(subscribers.subscribers, handler)
}


Both the stub and the impl can be used as the interface, hence our code can be run with the real NATS API, as well as with the stub.






Monday, December 4, 2023

NATS cluster setup in a kubernetes environment




In this post we will review the steps to "nationalize" a NATS cluster installation as part of our own kubernetes solution.


Using a 3rd-party technology as part of a kubernetes solution usually requires that this technology is installed as part of the solution itself, using a single helm chart for the entire solution. This means we need to create our own docker images to wrap the 3rd-party docker images, and integrate the 3rd-party helm chart into our solution helm charts. Let's review the steps to accomplish this for a NATS cluster.


Docker Images

First, start by wrapping up the NATS docker images. The core NATS server uses 2 images. In addition we will wrap the nats-box images which supplies CLI utilities.


The required docker files are:

NATS reloader Dockerfile:

ARG dockerCacheRepo
FROM ${dockerCacheRepo}natsio/nats-server-config-reloader:0.14.0


NATS container Dockerfile:

ARG dockerCacheRepo
FROM ${dockerCacheRepo}nats:2.10.5-alpine


NATS box Dockerfile:

ARG dockerCacheRepo
FROM ${dockerCacheRepo}natsio/nats-box:0.14.1


Notice that we use a build argument: dockerCacheRepo, which should be sent to the docker build command using the argument:

--build-arg dockerCacheRepo=https://my.docker.proxy.com

This enables us to use a docker proxy server and avoid the docker hub download limit.


The docker images should be tagged using our own solution naming, for example:

my-project-repo.com/solution1/nats-container:0.0.1


Helm Chart

To create templates for the NATS deployment, we using the official NATS chart:

helm repo add nats https://nats-io.github.io/k8s/helm/charts/
helm template -f ./helm-values.yaml nats nats/nats --output-dir ./target


and the helm-values.yaml file is:

fullnameOverride: solution1-nats
reloader:
image:
repository: my-project-repo.com/solution1/nats-reloader/dev
tag: latest
pullPolicy: IfNotPresent
container:
image:
repository: my-project-repo.com/solution1/nats-container/dev
tag: latest
pullPolicy: IfNotPresent
natsBox:
container:
image:
pullPolicy: IfNotPresent
repository: my-project-repo.com/solution1/nats-box/dev
tag: latest
config:
cluster:
enabled: true
port: 6222
replicas: 2
nats:
port: 4222
monitor:
port: 8222

 

This creates multiple files under the target folder. This files are the templates that should be integrated into our solution, probably as one of the sub charts. An example of one of these files containing the NATS deployment is below.


---
# Source: nats/templates/nats-box/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/component: nats-box
app.kubernetes.io/instance: nats
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: nats
app.kubernetes.io/version: 2.10.5
helm.sh/chart: nats-1.1.5
name: solution1-nats-box
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/component: nats-box
app.kubernetes.io/instance: nats
app.kubernetes.io/name: nats
template:
metadata:
labels:
app.kubernetes.io/component: nats-box
app.kubernetes.io/instance: nats
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: nats
app.kubernetes.io/version: 2.10.5
helm.sh/chart: nats-1.1.5
spec:
containers:
- args:
- trap true INT TERM; sleep infinity & wait
command:
- sh
- -ec
- |
work_dir="$(pwd)"
mkdir -p "$XDG_CONFIG_HOME/nats"
cd "$XDG_CONFIG_HOME/nats"
if ! [ -s context ]; then
ln -s /etc/nats-contexts context
fi
if ! [ -f context.txt ]; then
echo -n "default" > context.txt
fi
cd "$work_dir"
exec sh -ec "$0"
image: my-project-repo.com/solution1/nats-box/dev:latest
imagePullPolicy: IfNotPresent
name: nats-box
volumeMounts:
- mountPath: /etc/nats-contexts
name: contexts
enableServiceLinks: false
volumes:
- name: contexts
secret:
secretName: solution1-nats-box-contexts