├── .devcontainer
├── devcontainer.json
├── init.sh
└── pg_hba.conf
├── .editorconfig
├── .gitattributes
├── .github
└── workflows
│ ├── ci.yml
│ ├── pack.yml
│ └── test-report.yml
├── .gitignore
├── .vscode
└── settings.json
├── COPYING
├── COPYING.LESSER
├── Hangfire.PostgreSql.sln
├── Hangfire.PostgreSql.sln.DotSettings
├── LICENSE.md
├── README.md
├── SECURITY.md
├── src
├── Common
│ ├── Hangfire.ruleset
│ └── Hangfire.targets
└── Hangfire.PostgreSql
│ ├── CountersAggregator.cs
│ ├── EnqueuedAndFetchedCountDto.cs
│ ├── Entities
│ ├── JobParameter.cs
│ ├── Server.cs
│ ├── ServerData.cs
│ ├── SqlHash.cs
│ ├── SqlJob.cs
│ └── SqlState.cs
│ ├── EnvironmentHelpers.cs
│ ├── ExpirationManager.cs
│ ├── Factories
│ ├── ExistingNpgsqlConnectionFactory.cs
│ ├── NpgsqlConnectionFactory.cs
│ └── NpgsqlInstanceConnectionFactoryBase.cs
│ ├── Hangfire.PostgreSql.csproj
│ ├── IConnectionFactory.cs
│ ├── IPersistentJobQueue.cs
│ ├── IPersistentJobQueueMonitoringApi.cs
│ ├── IPersistentJobQueueProvider.cs
│ ├── JsonParameter.cs
│ ├── PersistentJobQueueProviderCollection.cs
│ ├── PostgreSqlBootstrapperConfigurationExtensions.cs
│ ├── PostgreSqlBootstrapperOptions.cs
│ ├── PostgreSqlConnection.cs
│ ├── PostgreSqlDistributedLock.cs
│ ├── PostgreSqlDistributedLockException.cs
│ ├── PostgreSqlFetchedJob.cs
│ ├── PostgreSqlHeartbeatProcess.cs
│ ├── PostgreSqlJobQueue.cs
│ ├── PostgreSqlJobQueueMonitoringApi.cs
│ ├── PostgreSqlJobQueueProvider.cs
│ ├── PostgreSqlMonitoringApi.cs
│ ├── PostgreSqlObjectsInstaller.cs
│ ├── PostgreSqlStorage.cs
│ ├── PostgreSqlStorageOptions.cs
│ ├── PostgreSqlWriteOnlyTransaction.cs
│ ├── Properties
│ ├── Annotations.cs
│ └── AssemblyInfo.cs
│ ├── Scripts
│ ├── Install.v10.sql
│ ├── Install.v11.sql
│ ├── Install.v12.sql
│ ├── Install.v13.sql
│ ├── Install.v14.sql
│ ├── Install.v15.sql
│ ├── Install.v16.sql
│ ├── Install.v17.sql
│ ├── Install.v18.sql
│ ├── Install.v19.sql
│ ├── Install.v20.sql
│ ├── Install.v21.sql
│ ├── Install.v22.sql
│ ├── Install.v23.sql
│ ├── Install.v3.sql
│ ├── Install.v4.sql
│ ├── Install.v5.sql
│ ├── Install.v6.sql
│ ├── Install.v7.sql
│ ├── Install.v8.sql
│ └── Install.v9.sql
│ └── Utils
│ ├── AutoResetEventRegistry.cs
│ ├── DbConnectionExtensions.cs
│ ├── ExceptionTypeHelper.cs
│ ├── TimestampHelper.cs
│ ├── TransactionHelpers.cs
│ └── TryExecute.cs
└── tests
└── Hangfire.PostgreSql.Tests
├── AssemblyAttributes.cs
├── CountersAggregatorFacts.cs
├── Entities
└── TestJob.cs
├── ExpirationManagerFacts.cs
├── FirstClassQueueFeatureSupportTests.cs
├── GlobalSuppressions.cs
├── Hangfire.PostgreSql.Tests.csproj
├── PersistentJobQueueProviderCollectionFacts.cs
├── PostgreSqlConnectionFacts.cs
├── PostgreSqlDistributedLockFacts.cs
├── PostgreSqlFetchedJobFacts.cs
├── PostgreSqlInstallerFacts.cs
├── PostgreSqlJobQueueFacts.cs
├── PostgreSqlMonitoringApiFacts.cs
├── PostgreSqlStorageFacts.cs
├── PostgreSqlStorageOptionsFacts.cs
├── PostgreSqlWriteOnlyTransactionFacts.cs
├── Scripts
└── Clean.sql
└── Utils
├── CleanDatabaseAttribute.cs
├── ConnectionUtils.cs
├── DefaultConnectionFactory.cs
├── Helper.cs
├── PostgreSqlStorageExtensions.cs
├── PostgreSqlStorageFixture.cs
└── PostgreSqlTestObjectsInitializer.cs
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "image": "mcr.microsoft.com/devcontainers/universal:2",
3 | "features": {
4 | "ghcr.io/devcontainers/features/dotnet:1": {}
5 | },
6 | "customizations": {
7 | "vscode": {
8 | "extensions": [
9 | "ms-dotnettools.csharp",
10 | "eamodio.gitlens",
11 | "formulahendry.dotnet-test-explorer",
12 | "mtxr.sqltools-driver-pg"
13 | ]
14 | }
15 | },
16 | "containerEnv": {
17 | "DOTNET_CLI_HOME": "/tmp/DOTNET_CLI_HOME"
18 | },
19 | "postCreateCommand": "./.devcontainer/init.sh",
20 | "postStartCommand": "sudo service postgresql start"
21 | }
22 |
--------------------------------------------------------------------------------
/.devcontainer/init.sh:
--------------------------------------------------------------------------------
1 | sudo apt update
2 | sudo apt install postgresql postgresql-contrib -y
3 | sudo cp ./.devcontainer/pg_hba.conf /etc/postgresql/12/main/pg_hba.conf
4 | sudo chown postgres:postgres /etc/postgresql/12/main/pg_hba.conf
5 | sudo service postgresql start
6 |
7 | sudo psql -U postgres -c "ALTER USER postgres PASSWORD 'password';"
8 |
9 | dotnet restore
--------------------------------------------------------------------------------
/.devcontainer/pg_hba.conf:
--------------------------------------------------------------------------------
1 | # PostgreSQL Client Authentication Configuration File
2 | # ===================================================
3 | #
4 | # Refer to the "Client Authentication" section in the PostgreSQL
5 | # documentation for a complete description of this file. A short
6 | # synopsis follows.
7 | #
8 | # This file controls: which hosts are allowed to connect, how clients
9 | # are authenticated, which PostgreSQL user names they can use, which
10 | # databases they can access. Records take one of these forms:
11 | #
12 | # local DATABASE USER METHOD [OPTIONS]
13 | # host DATABASE USER ADDRESS METHOD [OPTIONS]
14 | # hostssl DATABASE USER ADDRESS METHOD [OPTIONS]
15 | # hostnossl DATABASE USER ADDRESS METHOD [OPTIONS]
16 | # hostgssenc DATABASE USER ADDRESS METHOD [OPTIONS]
17 | # hostnogssenc DATABASE USER ADDRESS METHOD [OPTIONS]
18 | #
19 | # (The uppercase items must be replaced by actual values.)
20 | #
21 | # The first field is the connection type: "local" is a Unix-domain
22 | # socket, "host" is either a plain or SSL-encrypted TCP/IP socket,
23 | # "hostssl" is an SSL-encrypted TCP/IP socket, and "hostnossl" is a
24 | # non-SSL TCP/IP socket. Similarly, "hostgssenc" uses a
25 | # GSSAPI-encrypted TCP/IP socket, while "hostnogssenc" uses a
26 | # non-GSSAPI socket.
27 | #
28 | # DATABASE can be "all", "sameuser", "samerole", "replication", a
29 | # database name, or a comma-separated list thereof. The "all"
30 | # keyword does not match "replication". Access to replication
31 | # must be enabled in a separate record (see example below).
32 | #
33 | # USER can be "all", a user name, a group name prefixed with "+", or a
34 | # comma-separated list thereof. In both the DATABASE and USER fields
35 | # you can also write a file name prefixed with "@" to include names
36 | # from a separate file.
37 | #
38 | # ADDRESS specifies the set of hosts the record matches. It can be a
39 | # host name, or it is made up of an IP address and a CIDR mask that is
40 | # an integer (between 0 and 32 (IPv4) or 128 (IPv6) inclusive) that
41 | # specifies the number of significant bits in the mask. A host name
42 | # that starts with a dot (.) matches a suffix of the actual host name.
43 | # Alternatively, you can write an IP address and netmask in separate
44 | # columns to specify the set of hosts. Instead of a CIDR-address, you
45 | # can write "samehost" to match any of the server's own IP addresses,
46 | # or "samenet" to match any address in any subnet that the server is
47 | # directly connected to.
48 | #
49 | # METHOD can be "trust", "reject", "md5", "password", "scram-sha-256",
50 | # "gss", "sspi", "ident", "peer", "pam", "ldap", "radius" or "cert".
51 | # Note that "password" sends passwords in clear text; "md5" or
52 | # "scram-sha-256" are preferred since they send encrypted passwords.
53 | #
54 | # OPTIONS are a set of options for the authentication in the format
55 | # NAME=VALUE. The available options depend on the different
56 | # authentication methods -- refer to the "Client Authentication"
57 | # section in the documentation for a list of which options are
58 | # available for which authentication methods.
59 | #
60 | # Database and user names containing spaces, commas, quotes and other
61 | # special characters must be quoted. Quoting one of the keywords
62 | # "all", "sameuser", "samerole" or "replication" makes the name lose
63 | # its special character, and just match a database or username with
64 | # that name.
65 | #
66 | # This file is read on server startup and when the server receives a
67 | # SIGHUP signal. If you edit the file on a running system, you have to
68 | # SIGHUP the server for the changes to take effect, run "pg_ctl reload",
69 | # or execute "SELECT pg_reload_conf()".
70 | #
71 | # Put your actual configuration here
72 | # ----------------------------------
73 | #
74 | # If you want to allow non-local connections, you need to add more
75 | # "host" records. In that case you will also need to make PostgreSQL
76 | # listen on a non-local interface via the listen_addresses
77 | # configuration parameter, or via the -i or -h command line switches.
78 |
79 |
80 |
81 |
82 | # DO NOT DISABLE!
83 | # If you change this first entry you will need to make sure that the
84 | # database superuser can access the database using some other method.
85 | # Noninteractive access to all databases is required during automatic
86 | # maintenance (custom daily cronjobs, replication, and similar tasks).
87 | #
88 | # Database administrative login by Unix domain socket
89 | local all postgres trust
90 | local all postgres md5
91 |
92 | # TYPE DATABASE USER ADDRESS METHOD
93 |
94 | # "local" is for Unix domain socket connections only
95 | local all all peer
96 | # IPv4 local connections:
97 | host all all 127.0.0.1/32 md5
98 | # IPv6 local connections:
99 | host all all ::1/128 md5
100 | # Allow replication connections from localhost, by a user with the
101 | # replication privilege.
102 | local replication all peer
103 | host replication all 127.0.0.1/32 md5
104 | host replication all ::1/128 md5
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Verification build
2 |
3 | on:
4 | workflow_dispatch:
5 | pull_request:
6 | branches: ["master"]
7 | paths:
8 | - "src/**"
9 | - "test/**"
10 |
11 | permissions: write-all
12 |
13 | jobs:
14 | build-and-test:
15 | runs-on: ubuntu-latest
16 |
17 | services:
18 | postgres:
19 | image: postgres
20 | env:
21 | POSTGRES_PASSWORD: postgres
22 | POSTGRES_HOST_AUTH_METHOD: trust
23 | TZ: UTC+13
24 | PGTZ: UTC+13
25 | options: >-
26 | --health-cmd pg_isready
27 | --health-interval 10s
28 | --health-timeout 5s
29 | --health-retries 5
30 | ports:
31 | - 5432:5432
32 |
33 | steps:
34 | - name: Checkout
35 | uses: actions/checkout@v4
36 | with:
37 | fetch-depth: 0
38 | - name: Setup .NET
39 | uses: actions/setup-dotnet@v4
40 | with:
41 | dotnet-version: 9.0.x
42 | - name: Restore dependencies
43 | run: dotnet restore
44 | - name: Build
45 | run: dotnet build --no-restore
46 | - name: Test
47 | run: dotnet test --no-build --verbosity normal --logger "trx;LogFileName=TestResults.trx"
48 | - name: Test Report
49 | uses: actions/upload-artifact@v4
50 | if: success() || failure()
51 | with:
52 | name: test-results
53 | path: "**/TestResults.trx"
54 |
--------------------------------------------------------------------------------
/.github/workflows/pack.yml:
--------------------------------------------------------------------------------
1 | name: Build and tests
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches: ["master"]
7 | paths: ["src/**"]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v4
16 | with:
17 | fetch-depth: 0
18 | - name: Setup .NET
19 | uses: actions/setup-dotnet@v4
20 | with:
21 | dotnet-version: 9.0.x
22 | - name: Restore dependencies
23 | run: dotnet restore
24 | - name: Build
25 | run: dotnet build --no-restore -c Release
26 | - name: Pack
27 | run: dotnet pack -c Release --no-build --output publish
28 | - name: Upload a Build Artifact
29 | uses: actions/upload-artifact@v4
30 | with:
31 | name: Package
32 | path: publish/**.nupkg
33 |
34 | test:
35 | permissions: write-all
36 | runs-on: ubuntu-latest
37 |
38 | services:
39 | postgres:
40 | image: postgres
41 | env:
42 | POSTGRES_PASSWORD: postgres
43 | POSTGRES_HOST_AUTH_METHOD: trust
44 | TZ: UTC+13
45 | PGTZ: UTC+13
46 | options: >-
47 | --health-cmd pg_isready
48 | --health-interval 10s
49 | --health-timeout 5s
50 | --health-retries 5
51 | ports:
52 | - 5432:5432
53 |
54 | steps:
55 | - name: Checkout
56 | uses: actions/checkout@v4
57 | with:
58 | fetch-depth: 0
59 | - name: Setup .NET
60 | uses: actions/setup-dotnet@v4
61 | with:
62 | dotnet-version: 9.0.x
63 | - name: Restore dependencies
64 | run: dotnet restore
65 | - name: Build
66 | run: dotnet build -c Release --no-restore
67 | - name: Test
68 | run: dotnet test -c Release --no-build --verbosity normal --logger "trx;LogFileName=TestResults.trx" || true
69 | - name: Test Report
70 | uses: dorny/test-reporter@v1
71 | if: always()
72 | with:
73 | name: Test results
74 | path: "**/TestResults.trx"
75 | reporter: dotnet-trx
76 | fail-on-error: true
77 |
--------------------------------------------------------------------------------
/.github/workflows/test-report.yml:
--------------------------------------------------------------------------------
1 | name: Test report
2 |
3 | on:
4 | workflow_run:
5 | workflows: ['Verification build', 'Build and tests']
6 | types:
7 | - completed
8 |
9 | permissions:
10 | id-token: write
11 | contents: read
12 | checks: write
13 |
14 | jobs:
15 | report:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: dorny/test-reporter@v1
19 | with:
20 | artifact: test-results
21 | name: Test results
22 | path: "**/TestResults.trx"
23 | reporter: dotnet-trx
24 | fail-on-error: false
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | #################
2 | ## Eclipse
3 | #################
4 |
5 | *.pydevproject
6 | .project
7 | .metadata
8 | bin/
9 | tmp/
10 | *.tmp
11 | *.bak
12 | *.swp
13 | *~.nib
14 | local.properties
15 | .classpath
16 | .settings/
17 | .loadpath
18 |
19 | # External tool builders
20 | .externalToolBuilders/
21 |
22 | # Locally stored "Eclipse launch configurations"
23 | *.launch
24 |
25 | # CDT-specific
26 | .cproject
27 |
28 | # PDT-specific
29 | .buildpath
30 |
31 |
32 | #################
33 | ## Visual Studio
34 | #################
35 |
36 | ## Ignore Visual Studio temporary files, build results, and
37 | ## files generated by popular Visual Studio add-ons.
38 |
39 | # User-specific files
40 | *.suo
41 | *.user
42 | *.sln.docstates
43 |
44 | packages/
45 |
46 | # Build results
47 |
48 | [Bb]uild/
49 | [Dd]ebug/
50 | [Rr]elease/
51 | x64/
52 | [Bb]in/
53 | [Oo]bj/
54 |
55 | # MSTest test Results
56 | [Tt]est[Rr]esult*/
57 | [Bb]uild[Ll]og.*
58 |
59 | *_i.c
60 | *_p.c
61 | *.ilk
62 | *.meta
63 | *.obj
64 | *.pch
65 | *.pdb
66 | *.pgc
67 | *.pgd
68 | *.rsp
69 | *.sbr
70 | *.tlb
71 | *.tli
72 | *.tlh
73 | *.tmp
74 | *.tmp_proj
75 | *.log
76 | *.vspscc
77 | *.vssscc
78 | .builds
79 | *.pidb
80 | *.log
81 | *.scc
82 |
83 | # Visual C++ cache files
84 | ipch/
85 | *.aps
86 | *.ncb
87 | *.opensdf
88 | *.sdf
89 | *.cachefile
90 |
91 | # Visual Studio profiler
92 | *.psess
93 | *.vsp
94 | *.vspx
95 |
96 | # Guidance Automation Toolkit
97 | *.gpState
98 |
99 | # ReSharper is a .NET coding add-in
100 | _ReSharper*/
101 | *.[Rr]e[Ss]harper
102 |
103 | # TeamCity is a build add-in
104 | _TeamCity*
105 |
106 | # DotCover is a Code Coverage Tool
107 | *.dotCover
108 |
109 | # NCrunch
110 | *.ncrunch*
111 | .*crunch*.local.xml
112 |
113 | # Installshield output folder
114 | [Ee]xpress/
115 |
116 | # DocProject is a documentation generator add-in
117 | DocProject/buildhelp/
118 | DocProject/Help/*.HxT
119 | DocProject/Help/*.HxC
120 | DocProject/Help/*.hhc
121 | DocProject/Help/*.hhk
122 | DocProject/Help/*.hhp
123 | DocProject/Help/Html2
124 | DocProject/Help/html
125 |
126 | # Click-Once directory
127 | publish/
128 |
129 | # Publish Web Output
130 | *.Publish.xml
131 | *.pubxml
132 |
133 | # NuGet Packages Directory
134 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line
135 | #packages/
136 |
137 | # Windows Azure Build Output
138 | csx
139 | *.build.csdef
140 |
141 | # Windows Store app package directory
142 | AppPackages/
143 |
144 | # Others
145 | sql/
146 | *.Cache
147 | ClientBin/
148 | [Ss]tyle[Cc]op.*
149 | ~$*
150 | *~
151 | *.dbmdl
152 | *.[Pp]ublish.xml
153 | *.pfx
154 | *.publishsettings
155 |
156 | # RIA/Silverlight projects
157 | Generated_Code/
158 |
159 | # Backup & report files from converting an old project file to a newer
160 | # Visual Studio version. Backup files are not needed, because we have git ;-)
161 | _UpgradeReport_Files/
162 | Backup*/
163 | UpgradeLog*.XML
164 | UpgradeLog*.htm
165 |
166 | # SQL Server files
167 | App_Data/*.mdf
168 | App_Data/*.ldf
169 |
170 | #############
171 | ## Windows detritus
172 | #############
173 |
174 | # Windows image file caches
175 | Thumbs.db
176 | ehthumbs.db
177 |
178 | # Folder config file
179 | Desktop.ini
180 |
181 | # Recycle Bin used on file shares
182 | $RECYCLE.BIN/
183 |
184 | # Mac crap
185 | .DS_Store
186 |
187 |
188 | #############
189 | ## Python
190 | #############
191 |
192 | *.py[co]
193 |
194 | # Packages
195 | *.egg
196 | *.egg-info
197 | dist/
198 | eggs/
199 | parts/
200 | var/
201 | sdist/
202 | develop-eggs/
203 | .installed.cfg
204 |
205 | # Installer logs
206 | pip-log.txt
207 |
208 | # Unit test / coverage reports
209 | .coverage
210 | .tox
211 |
212 | #Translations
213 | *.mo
214 |
215 | #Mr Developer
216 | .mr.developer.cfg
217 |
218 | #Sphinx Built Docs
219 | docs/_build
220 |
221 | #Jekyll site builds
222 | _site
223 | *.userprefs
224 | src/Hangfire.PostgreSql/.vs/restore.dg
225 | src/Hangfire.PostgreSql/project.lock.json
226 | */**/project.lock.json
227 | tools
228 | .vs
229 | .idea
230 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "sqltools.connections": [
3 | {
4 | "previewLimit": 50,
5 | "server": "localhost",
6 | "port": 5432,
7 | "driver": "PostgreSQL",
8 | "name": "localhost",
9 | "database": "postgres",
10 | "username": "postgres",
11 | "password": "password"
12 | }
13 | ]
14 | }
--------------------------------------------------------------------------------
/COPYING:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/COPYING.LESSER:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/Hangfire.PostgreSql.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.5.33424.131
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{766BE831-F758-46BC-AFD3-BBEEFE0F686F}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5CA38188-92EE-453C-A04E-A506DF15A787}"
9 | EndProject
10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0D30A51B-814F-474E-93B8-44E9C155255C}"
11 | EndProject
12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hangfire.PostgreSql", "src\Hangfire.PostgreSql\Hangfire.PostgreSql.csproj", "{3E4DBC41-F38E-4D1C-A6A7-206A132A29D6}"
13 | EndProject
14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hangfire.PostgreSql.Tests", "tests\Hangfire.PostgreSql.Tests\Hangfire.PostgreSql.Tests.csproj", "{6044A48D-730B-4D1F-B03A-EB2B458DAF53}"
15 | EndProject
16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{AAA78654-9846-4870-A13C-D9DBAF0792C4}"
17 | ProjectSection(SolutionItems) = preProject
18 | .github\workflows\ci.yml = .github\workflows\ci.yml
19 | .github\workflows\pack.yml = .github\workflows\pack.yml
20 | EndProjectSection
21 | EndProject
22 | Global
23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
24 | Debug|Any CPU = Debug|Any CPU
25 | Release|Any CPU = Release|Any CPU
26 | EndGlobalSection
27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
28 | {3E4DBC41-F38E-4D1C-A6A7-206A132A29D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
29 | {3E4DBC41-F38E-4D1C-A6A7-206A132A29D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
30 | {3E4DBC41-F38E-4D1C-A6A7-206A132A29D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
31 | {3E4DBC41-F38E-4D1C-A6A7-206A132A29D6}.Release|Any CPU.Build.0 = Release|Any CPU
32 | {6044A48D-730B-4D1F-B03A-EB2B458DAF53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33 | {6044A48D-730B-4D1F-B03A-EB2B458DAF53}.Debug|Any CPU.Build.0 = Debug|Any CPU
34 | {6044A48D-730B-4D1F-B03A-EB2B458DAF53}.Release|Any CPU.ActiveCfg = Release|Any CPU
35 | {6044A48D-730B-4D1F-B03A-EB2B458DAF53}.Release|Any CPU.Build.0 = Release|Any CPU
36 | EndGlobalSection
37 | GlobalSection(SolutionProperties) = preSolution
38 | HideSolutionNode = FALSE
39 | EndGlobalSection
40 | GlobalSection(NestedProjects) = preSolution
41 | {3E4DBC41-F38E-4D1C-A6A7-206A132A29D6} = {0D30A51B-814F-474E-93B8-44E9C155255C}
42 | {6044A48D-730B-4D1F-B03A-EB2B458DAF53} = {766BE831-F758-46BC-AFD3-BBEEFE0F686F}
43 | {AAA78654-9846-4870-A13C-D9DBAF0792C4} = {5CA38188-92EE-453C-A04E-A506DF15A787}
44 | EndGlobalSection
45 | GlobalSection(ExtensibilityGlobals) = postSolution
46 | SolutionGuid = {F7E32105-7F61-4127-8517-5E4275B9CABE}
47 | EndGlobalSection
48 | EndGlobal
49 |
--------------------------------------------------------------------------------
/Hangfire.PostgreSql.sln.DotSettings:
--------------------------------------------------------------------------------
1 |
2 | True
3 | True
4 | True
5 | True
6 | True
7 | True
8 | <data><IncludeFilters /><ExcludeFilters><Filter ModuleMask="Hangfire.Core.Tests" ModuleVersionMask="*" ClassMask="*" FunctionMask="*" IsEnabled="True" /><Filter ModuleMask="Hangfire.SqlServer.Tests" ModuleVersionMask="*" ClassMask="*" FunctionMask="*" IsEnabled="True" /></ExcludeFilters></data>
9 | <data />
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | License
2 | ========
3 |
4 | Copyright © 2014-2022 Frank Hommers https://github.com/frankhommers/Hangfire.PostgreSql and others (Burhan Irmikci (barhun), Zachary Sims(
5 | zsims), kgamecarter, Stafford Williams (staff0rd), briangweber, Viktor Svyatokha (ahydrax), Christopher Dresel (Dresel),
6 | Vytautas Kasparavičius (vytautask), Vincent Vrijburg, David Roth (davidroth).
7 |
8 | Hangfire.PostgreSql is an Open Source project licensed under the terms of the LGPLv3 license. Please
9 | see http://www.gnu.org/licenses/lgpl-3.0.html for license text or COPYING.LESSER file distributed with the source code.
10 |
11 | This work is based on the work of Sergey Odinokov, author of Hangfire.
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Hangfire.PostgreSql
2 |
3 | [](https://github.com/hangfire-postgres/Hangfire.PostgreSql/actions/workflows/pack.yml) [](https://github.com/hangfire-postgres/Hangfire.PostgreSql/releases/latest) [](https://www.nuget.org/packages/Hangfire.PostgreSql)
4 |
5 | This is an plugin to the Hangfire to enable PostgreSQL as a storage system.
6 | Read about hangfire here: https://github.com/HangfireIO/Hangfire#overview
7 | and here: http://hangfire.io/
8 |
9 | ## Instructions
10 |
11 | ### For .NET
12 |
13 | Install Hangfire, see https://github.com/HangfireIO/Hangfire#installation
14 |
15 | Download all files from this repository, add the Hangfire.PostgreSql.csproj to your solution.
16 | Reference it in your project, and you are ready to go by using:
17 |
18 | ```csharp
19 | app.UseHangfireServer(new BackgroundJobServerOptions(),
20 | new PostgreSqlStorage(""));
21 | app.UseHangfireDashboard();
22 | ```
23 |
24 | ### For ASP.NET Core
25 |
26 | First, NuGet package needs installation.
27 |
28 | - Hangfire.AspNetCore
29 | - Hangfire.PostgreSql (Uses Npgsql 6)
30 | - Hangfire.PostgreSql.Npgsql5 (Uses Npgsql 5)
31 |
32 | Historically both packages were functionally the same up until the package version 1.9.11, the only difference was the underlying Npgsql dependency version. Afterwards, the support for Npgsql v5 has been dropped and now minimum required version is 6.0.0.
33 |
34 | In `Startup.cs` _ConfigureServices(IServiceCollection services)_ method add the following line:
35 |
36 | ```csharp
37 | services.AddHangfire(config =>
38 | config.UsePostgreSqlStorage(c =>
39 | c.UseNpgsqlConnection(Configuration.GetConnectionString("HangfireConnection"))));
40 | ```
41 |
42 | In Configure method, add these two lines:
43 |
44 | ```csharp
45 | app.UseHangfireServer();
46 | app.UseHangfireDashboard();
47 | ```
48 |
49 | And... That's it. You are ready to go.
50 |
51 | If you encounter any issues/bugs or have idea of a feature regarding Hangfire.Postgresql, [create us an issue](https://github.com/hangfire-postgres/Hangfire.PostgreSql/issues/new). Thanks!
52 |
53 | ### Enabling SSL support
54 |
55 | SSL support can be enabled for Hangfire.PostgreSql library using the following mechanism:
56 |
57 | ```csharp
58 | config.UsePostgreSqlStorage(c =>
59 | c.UseNpgsqlConnection(
60 | Configuration.GetConnectionString("HangfireConnection"), // connection string,
61 | connection => // connection setup - gets called after instantiating the connection and before any calls to DB are made
62 | {
63 | connection.ProvideClientCertificatesCallback += clientCerts =>
64 | {
65 | clientCerts.Add(X509Certificate.CreateFromCertFile("[CERT_FILENAME]"));
66 | };
67 | }
68 | )
69 | );
70 | ```
71 | ### Queue processing
72 |
73 | Similar to `Hangfire.SqlServer`, queues are processed in alphabetical order. Given the following example
74 |
75 | ```csharp
76 | var options = new BackgroundJobServerOptions
77 | {
78 | Queues = new[] { "general-queue", "very-fast-queue", "a-long-running-queue" }
79 | };
80 | app.UseHangfireServer(options);
81 | ```
82 |
83 | this provider would first process jobs in `a-long-running-queue`, then `general-queue` and lastly `very-fast-queue`.
84 |
85 | ### License
86 |
87 | Copyright © 2014-2024 Frank Hommers https://github.com/hangfire-postgres/Hangfire.PostgreSql.
88 |
89 | Collaborators:
90 | Frank Hommers (frankhommers), Vytautas Kasparavičius (vytautask), Žygimantas Arūna (azygis)
91 |
92 | Contributors:
93 | Andrew Armstrong (Plasma), Burhan Irmikci (barhun), Zachary Sims(zsims), kgamecarter, Stafford Williams (staff0rd), briangweber, Viktor Svyatokha (ahydrax), Christopher Dresel (Dresel), Vincent Vrijburg, David Roth (davidroth) and Ivan Tiniakov (Tinyakov).
94 |
95 | Hangfire.PostgreSql is an Open Source project licensed under the terms of the LGPLv3 license. Please see http://www.gnu.org/licenses/lgpl-3.0.html for license text or COPYING.LESSER file distributed with the source code.
96 |
97 | This work is based on the work of Sergey Odinokov, author of Hangfire.
98 |
99 | ### Related Projects
100 |
101 | - [Hangfire.Core](https://github.com/HangfireIO/Hangfire)
102 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | Use this section to tell people about which versions of your project are
6 | currently being supported with security updates.
7 |
8 | | Version | Supported |
9 | | ------- | ------------------ |
10 | | 1.6.3 | :white_check_mark: |
11 | | < 1.6.3 | :x: |
12 |
13 |
14 | ## Reporting a Vulnerability
15 |
16 | Do not create a new issue ticket with the detailed information of the vulnerability under any circumstances!
17 |
18 | All vulnerabilities can be reported directly to the main project maintainers (currently @frankfommers and @vytautask) directly
19 | via email (frank-hfpg[at]hommers[dot]nl or vytautaskasp[at]gmail[dot]com respectively). We should get back to you in 24 hours. Please include as much information as possible (if you have a POC - even better). All security issues have our highest attention and will be solved ASAP.
20 |
21 | You would help us out if you also include a fix in a pull request. That way we can quickly release a fix.
22 |
23 | Thank you for your understanding.
24 |
--------------------------------------------------------------------------------
/src/Common/Hangfire.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | $(ArtifactsDir)\$(MSBuildProjectName)
5 | $(ArtifactsDir)\$(MSBuildProjectName)\bin
6 |
7 |
8 |
9 | $(MSBuildThisFileDirectory)Hangfire.ruleset
10 | false
11 | 1591
12 | false
13 |
14 |
15 |
16 | $(DefineConstants);CODE_ANALYSIS
17 | 11.0
18 |
19 |
20 |
21 | $(DefineConstants);MONO
22 |
23 |
24 |
25 | $(DefineConstants);SIGNED
26 | true
27 | true
28 | $(KeyFile)
29 |
30 |
31 |
32 |
33 | GlobalSuppressions.cs
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/CountersAggregator.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.PostgreSql.
2 | // Copyright © 2014 Frank Hommers .
3 | //
4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.PostgreSql. If not, see .
16 | //
17 | // This work is based on the work of Sergey Odinokov, author of
18 | // Hangfire.
19 | //
20 | // Special thanks goes to him.
21 |
22 | using System;
23 | using System.Threading;
24 | using Dapper;
25 | using Hangfire.Common;
26 | using Hangfire.Logging;
27 | using Hangfire.Server;
28 |
29 | namespace Hangfire.PostgreSql
30 | {
31 | #pragma warning disable 618
32 | internal class CountersAggregator : IServerComponent
33 | #pragma warning restore 618
34 | {
35 | // This number should be high enough to aggregate counters efficiently,
36 | // but low enough to not to cause large amount of row locks to be taken.
37 | // Lock escalation to page locks may pause the background processing.
38 | private const int NumberOfRecordsInSinglePass = 1000;
39 |
40 | private static readonly TimeSpan _delayBetweenPasses = TimeSpan.FromMilliseconds(500);
41 |
42 | private readonly ILog _logger = LogProvider.For();
43 | private readonly TimeSpan _interval;
44 | private readonly PostgreSqlStorage _storage;
45 |
46 | public CountersAggregator(PostgreSqlStorage storage, TimeSpan interval)
47 | {
48 | _storage = storage ?? throw new ArgumentNullException(nameof(storage));
49 | _interval = interval;
50 | }
51 |
52 | public void Execute(CancellationToken cancellationToken)
53 | {
54 | _logger.Debug("Aggregating records in 'Counter' table...");
55 |
56 | int removedCount = 0;
57 |
58 | do
59 | {
60 | _storage.UseConnection(null, connection => {
61 | removedCount = connection.Execute(GetAggregationQuery(),
62 | new { now = DateTime.UtcNow, count = NumberOfRecordsInSinglePass },
63 | commandTimeout: 0);
64 | });
65 |
66 | if (removedCount < NumberOfRecordsInSinglePass)
67 | {
68 | continue;
69 | }
70 |
71 | cancellationToken.Wait(_delayBetweenPasses);
72 | cancellationToken.ThrowIfCancellationRequested();
73 | // ReSharper disable once LoopVariableIsNeverChangedInsideLoop
74 | } while (removedCount >= NumberOfRecordsInSinglePass);
75 |
76 | _logger.Trace("Records from the 'Counter' table aggregated.");
77 |
78 | cancellationToken.Wait(_interval);
79 | }
80 |
81 | private string GetAggregationQuery()
82 | {
83 | string schemaName = _storage.Options.SchemaName;
84 | return
85 | $"""
86 | BEGIN;
87 |
88 | INSERT INTO "{schemaName}"."aggregatedcounter" ("key", "value", "expireat")
89 | SELECT
90 | "key",
91 | SUM("value"),
92 | MAX("expireat")
93 | FROM "{schemaName}"."counter"
94 | GROUP BY "key"
95 | ON CONFLICT("key") DO UPDATE
96 | SET "value" = "aggregatedcounter"."value" + EXCLUDED."value", "expireat" = EXCLUDED."expireat";
97 |
98 | DELETE FROM "{schemaName}"."counter"
99 | WHERE "key" IN (
100 | SELECT "key" FROM "{schemaName}"."aggregatedcounter"
101 | );
102 |
103 | COMMIT;
104 | """;
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/EnqueuedAndFetchedCountDto.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.PostgreSql.
2 | // Copyright © 2014 Frank Hommers .
3 | //
4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.PostgreSql. If not, see .
16 | //
17 | // This work is based on the work of Sergey Odinokov, author of
18 | // Hangfire.
19 | //
20 | // Special thanks goes to him.
21 |
22 | namespace Hangfire.PostgreSql
23 | {
24 | public class EnqueuedAndFetchedCountDto
25 | {
26 | public long EnqueuedCount { get; set; }
27 | public long FetchedCount { get; set; }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Entities/JobParameter.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.PostgreSql.
2 | // Copyright © 2014 Frank Hommers .
3 | //
4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.PostgreSql. If not, see .
16 | //
17 | // This work is based on the work of Sergey Odinokov, author of
18 | // Hangfire.
19 | //
20 | // Special thanks goes to him.
21 |
22 | using Hangfire.PostgreSql.Properties;
23 |
24 | namespace Hangfire.PostgreSql.Entities
25 | {
26 | [UsedImplicitly]
27 | internal class JobParameter
28 | {
29 | public long JobId { get; set; }
30 | public string Name { get; set; }
31 | public string Value { get; set; }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Entities/Server.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.PostgreSql.
2 | // Copyright © 2014 Frank Hommers .
3 | //
4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.PostgreSql. If not, see .
16 | //
17 | // This work is based on the work of Sergey Odinokov, author of
18 | // Hangfire.
19 | //
20 | // Special thanks goes to him.
21 |
22 | using System;
23 | using Hangfire.PostgreSql.Properties;
24 |
25 | namespace Hangfire.PostgreSql.Entities
26 | {
27 | [UsedImplicitly]
28 | internal class Server
29 | {
30 | public string Id { get; set; }
31 | public string Data { get; set; }
32 | public DateTime LastHeartbeat { get; set; }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Entities/ServerData.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.PostgreSql.
2 | // Copyright © 2014 Frank Hommers .
3 | //
4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.PostgreSql. If not, see .
16 | //
17 | // This work is based on the work of Sergey Odinokov, author of
18 | // Hangfire.
19 | //
20 | // Special thanks goes to him.
21 |
22 | using System;
23 |
24 | namespace Hangfire.PostgreSql.Entities
25 | {
26 | internal class ServerData
27 | {
28 | public int WorkerCount { get; set; }
29 | public string[] Queues { get; set; }
30 | public DateTime? StartedAt { get; set; }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Entities/SqlHash.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.PostgreSql.
2 | // Copyright © 2014 Frank Hommers .
3 | //
4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.PostgreSql. If not, see .
16 | //
17 | // This work is based on the work of Sergey Odinokov, author of
18 | // Hangfire.
19 | //
20 | // Special thanks goes to him.
21 |
22 | using System;
23 | using Hangfire.PostgreSql.Properties;
24 |
25 | namespace Hangfire.PostgreSql.Entities
26 | {
27 | [UsedImplicitly]
28 | internal class SqlHash
29 | {
30 | public long Id { get; set; }
31 | public string Key { get; set; }
32 | public string Field { get; set; }
33 | public string Value { get; set; }
34 | public DateTime? ExpireAt { get; set; }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Entities/SqlJob.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.PostgreSql.
2 | // Copyright � 2014 Frank Hommers .
3 | //
4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.PostgreSql. If not, see .
16 | //
17 | // This work is based on the work of Sergey Odinokov, author of
18 | // Hangfire.
19 | //
20 | // Special thanks goes to him.
21 |
22 | using System;
23 | using Hangfire.PostgreSql.Properties;
24 |
25 | namespace Hangfire.PostgreSql.Entities
26 | {
27 | [UsedImplicitly]
28 | internal class SqlJob
29 | {
30 | public long Id { get; set; }
31 | public string InvocationData { get; set; }
32 | public string Arguments { get; set; }
33 | public DateTime CreatedAt { get; set; }
34 | public DateTime? ExpireAt { get; set; }
35 |
36 | public DateTime? FetchedAt { get; set; }
37 |
38 | public string StateName { get; set; }
39 | public string StateReason { get; set; }
40 | public string StateData { get; set; }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Entities/SqlState.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.PostgreSql.
2 | // Copyright © 2014 Frank Hommers .
3 | //
4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.PostgreSql. If not, see .
16 | //
17 | // This work is based on the work of Sergey Odinokov, author of
18 | // Hangfire.
19 | //
20 | // Special thanks goes to him.
21 |
22 | using System;
23 | using Hangfire.PostgreSql.Properties;
24 |
25 | namespace Hangfire.PostgreSql.Entities
26 | {
27 | [UsedImplicitly]
28 | internal class SqlState
29 | {
30 | public long JobId { get; set; }
31 | public string Name { get; set; }
32 | public string Reason { get; set; }
33 | public DateTime CreatedAt { get; set; }
34 | public string Data { get; set; }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/EnvironmentHelpers.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Hangfire.PostgreSql
4 | {
5 | internal class EnvironmentHelpers
6 | {
7 | private static bool? _isMono;
8 |
9 | public static bool IsMono()
10 | {
11 | return _isMono ??= Type.GetType("Mono.Runtime") != null;
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/ExpirationManager.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.PostgreSql.
2 | // Copyright © 2014 Frank Hommers .
3 | //
4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.PostgreSql. If not, see .
16 | //
17 | // This work is based on the work of Sergey Odinokov, author of
18 | // Hangfire.
19 | //
20 | // Special thanks goes to him.
21 |
22 | using System;
23 | using System.Data;
24 | using System.Globalization;
25 | using System.Threading;
26 | using Dapper;
27 | using Hangfire.Logging;
28 | using Hangfire.Server;
29 | using Hangfire.Storage;
30 |
31 | namespace Hangfire.PostgreSql
32 | {
33 | #pragma warning disable CS0618
34 | internal class ExpirationManager : IBackgroundProcess, IServerComponent
35 | #pragma warning restore CS0618
36 | {
37 | private const string DistributedLockKey = "locks:expirationmanager";
38 |
39 | private static readonly TimeSpan _defaultLockTimeout = TimeSpan.FromMinutes(5);
40 | private static readonly TimeSpan _delayBetweenPasses = TimeSpan.FromSeconds(1);
41 | private static readonly ILog _logger = LogProvider.GetLogger(typeof(ExpirationManager));
42 |
43 | private static readonly string[] _processedCounters = {
44 | "stats:succeeded",
45 | "stats:deleted",
46 | };
47 | private static readonly string[] _processedTables = {
48 | "aggregatedcounter",
49 | "counter",
50 | "job",
51 | "list",
52 | "set",
53 | "hash",
54 | };
55 |
56 | private readonly TimeSpan _checkInterval;
57 | private readonly PostgreSqlStorage _storage;
58 |
59 | public ExpirationManager(PostgreSqlStorage storage)
60 | : this(storage ?? throw new ArgumentNullException(nameof(storage)), storage.Options.JobExpirationCheckInterval) { }
61 |
62 | public ExpirationManager(PostgreSqlStorage storage, TimeSpan checkInterval)
63 | {
64 | _storage = storage ?? throw new ArgumentNullException(nameof(storage));
65 | _checkInterval = checkInterval;
66 | }
67 |
68 | public void Execute(BackgroundProcessContext context)
69 | {
70 | Execute(context.StoppingToken);
71 | }
72 |
73 | public void Execute(CancellationToken cancellationToken)
74 | {
75 | foreach (string table in _processedTables)
76 | {
77 | _logger.DebugFormat("Removing outdated records from table '{0}'...", table);
78 |
79 | UseConnectionDistributedLock(_storage, connection => {
80 | int removedCount;
81 | do
82 | {
83 | using IDbTransaction transaction = connection.BeginTransaction();
84 | removedCount = connection.Execute($@"
85 | DELETE FROM ""{_storage.Options.SchemaName}"".""{table}""
86 | WHERE ""id"" IN (
87 | SELECT ""id""
88 | FROM ""{_storage.Options.SchemaName}"".""{table}""
89 | WHERE ""expireat"" < NOW()
90 | LIMIT {_storage.Options.DeleteExpiredBatchSize.ToString(CultureInfo.InvariantCulture)}
91 | )", transaction: transaction);
92 |
93 | if (removedCount <= 0)
94 | {
95 | continue;
96 | }
97 |
98 | transaction.Commit();
99 | _logger.InfoFormat("Removed {0} outdated record(s) from '{1}' table.", removedCount, table);
100 |
101 | cancellationToken.WaitHandle.WaitOne(_delayBetweenPasses);
102 | cancellationToken.ThrowIfCancellationRequested();
103 | }
104 | while (removedCount != 0);
105 | });
106 | }
107 |
108 | AggregateCounters(cancellationToken);
109 | cancellationToken.WaitHandle.WaitOne(_checkInterval);
110 | }
111 |
112 | public override string ToString()
113 | {
114 | return "SQL Records Expiration Manager";
115 | }
116 |
117 | private void AggregateCounters(CancellationToken cancellationToken)
118 | {
119 | foreach (string processedCounter in _processedCounters)
120 | {
121 | AggregateCounter(processedCounter);
122 | cancellationToken.ThrowIfCancellationRequested();
123 | }
124 | }
125 |
126 | private void AggregateCounter(string counterName)
127 | {
128 | UseConnectionDistributedLock(_storage, connection => {
129 | using IDbTransaction transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted);
130 | string aggregateQuery = $@"
131 | WITH ""counters"" AS (
132 | DELETE FROM ""{_storage.Options.SchemaName}"".""counter""
133 | WHERE ""key"" = @Key
134 | AND ""expireat"" IS NULL
135 | RETURNING *
136 | )
137 |
138 | SELECT SUM(""value"") FROM ""counters"";
139 | ";
140 |
141 | long aggregatedValue = connection.ExecuteScalar(aggregateQuery, new { Key = counterName }, transaction);
142 | transaction.Commit();
143 |
144 | if (aggregatedValue > 0)
145 | {
146 | string insertQuery = $@"INSERT INTO ""{_storage.Options.SchemaName}"".""counter""(""key"", ""value"") VALUES (@Key, @Value);";
147 | connection.Execute(insertQuery, new { Key = counterName, Value = aggregatedValue });
148 | }
149 | });
150 | }
151 |
152 | private void UseConnectionDistributedLock(PostgreSqlStorage storage, Action action)
153 | {
154 | try
155 | {
156 | storage.UseConnection(null, connection => {
157 | PostgreSqlDistributedLock.Acquire(connection, DistributedLockKey, _defaultLockTimeout, _storage.Options);
158 |
159 | try
160 | {
161 | action(connection);
162 | }
163 | finally
164 | {
165 | PostgreSqlDistributedLock.Release(connection, DistributedLockKey, _storage.Options);
166 | }
167 | });
168 | }
169 | catch (DistributedLockTimeoutException e) when (e.Resource == DistributedLockKey)
170 | {
171 | // DistributedLockTimeoutException here doesn't mean that outdated records weren't removed.
172 | // It just means another Hangfire server did this work.
173 | _logger.Log(LogLevel.Debug,
174 | () =>
175 | $@"An exception was thrown during acquiring distributed lock on the {DistributedLockKey} resource within {_defaultLockTimeout.TotalSeconds} seconds. Outdated records were not removed. It will be retried in {_checkInterval.TotalSeconds} seconds.",
176 | e);
177 | }
178 | }
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Factories/ExistingNpgsqlConnectionFactory.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Npgsql;
3 |
4 | namespace Hangfire.PostgreSql.Factories;
5 |
6 | ///
7 | /// Connection factory that utilizes an already-existing .
8 | ///
9 | public sealed class ExistingNpgsqlConnectionFactory : NpgsqlInstanceConnectionFactoryBase
10 | {
11 | private readonly NpgsqlConnection _connection;
12 |
13 | ///
14 | /// Instantiates the factory using specified .
15 | ///
16 | /// to use.
17 | /// used for connection string verification.
18 | ///
19 | public ExistingNpgsqlConnectionFactory(NpgsqlConnection connection, PostgreSqlStorageOptions options) : base(options)
20 | {
21 | _connection = connection ?? throw new ArgumentNullException(nameof(connection));
22 | // To ensure valid connection string - throws internally
23 | SetupConnectionStringBuilder(_connection.ConnectionString);
24 | }
25 |
26 | ///
27 | public override NpgsqlConnection GetOrCreateConnection()
28 | {
29 | return _connection;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Factories/NpgsqlConnectionFactory.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Hangfire.Annotations;
3 | using Npgsql;
4 |
5 | namespace Hangfire.PostgreSql.Factories;
6 |
7 | ///
8 | /// Connection factory that creates a new based on the connection string.
9 | ///
10 | public sealed class NpgsqlConnectionFactory : NpgsqlInstanceConnectionFactoryBase
11 | {
12 | private readonly string _connectionString;
13 | [CanBeNull] private readonly Action _connectionSetup;
14 | [CanBeNull] private readonly Func _getConnectionString;
15 |
16 | ///
17 | /// Instantiates the factory using specified .
18 | ///
19 | /// Connection string.
20 | /// used for connection string verification.
21 | /// Optional additional connection setup action to be performed on the created .
22 | /// Throws if is null.
23 | public NpgsqlConnectionFactory(string connectionString, PostgreSqlStorageOptions options, [CanBeNull] Action connectionSetup = null) : base(options)
24 | {
25 | _connectionString = SetupConnectionStringBuilder(connectionString ?? throw new ArgumentNullException(nameof(connectionString))).ConnectionString;
26 | _connectionSetup = connectionSetup;
27 | }
28 |
29 | public NpgsqlConnectionFactory(Func getConnectionString, PostgreSqlStorageOptions options, [CanBeNull] Action connectionSetup = null) : this(getConnectionString.Invoke(), options, connectionSetup)
30 | {
31 | _getConnectionString = getConnectionString;
32 | }
33 |
34 | ///
35 | public override NpgsqlConnection GetOrCreateConnection()
36 | {
37 | var connectionString = _connectionString;
38 | if (_getConnectionString != null)
39 | {
40 | connectionString = SetupConnectionStringBuilder(_getConnectionString.Invoke()).ConnectionString;
41 | }
42 |
43 | NpgsqlConnection connection = new(connectionString);
44 | _connectionSetup?.Invoke(connection);
45 | return connection;
46 | }
47 | }
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Factories/NpgsqlInstanceConnectionFactoryBase.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Hangfire.Annotations;
3 | using Npgsql;
4 |
5 | namespace Hangfire.PostgreSql.Factories;
6 |
7 | public abstract class NpgsqlInstanceConnectionFactoryBase : IConnectionFactory
8 | {
9 | private readonly PostgreSqlStorageOptions _options;
10 | [CanBeNull] private NpgsqlConnectionStringBuilder _connectionStringBuilder;
11 | [CanBeNull] private string _connectionString;
12 |
13 | protected NpgsqlInstanceConnectionFactoryBase(PostgreSqlStorageOptions options)
14 | {
15 | _options = options ?? throw new ArgumentNullException(nameof(options));
16 | }
17 |
18 | ///
19 | /// Gets the connection string builder associated with the current instance.
20 | ///
21 | /// Throws if connection string builder has not been initialized.
22 | public NpgsqlConnectionStringBuilder ConnectionString =>
23 | _connectionStringBuilder ?? throw new InvalidOperationException("Connection string builder has not been initialized");
24 |
25 | protected NpgsqlConnectionStringBuilder SetupConnectionStringBuilder(string connectionString)
26 | {
27 | if (_connectionStringBuilder != null && string.Equals(_connectionString, connectionString, StringComparison.OrdinalIgnoreCase))
28 | {
29 | return _connectionStringBuilder;
30 | }
31 |
32 | try
33 | {
34 | _connectionString = connectionString;
35 | NpgsqlConnectionStringBuilder builder = new(connectionString);
36 |
37 | // The connection string must not be modified when transaction enlistment is enabled, otherwise it will cause
38 | // prepared transactions and probably fail when other statements (outside of hangfire) ran within the same
39 | // transaction. Also see #248.
40 | if (!_options.EnableTransactionScopeEnlistment && builder.Enlist)
41 | {
42 | throw new ArgumentException($"TransactionScope enlistment must be enabled by setting {nameof(PostgreSqlStorageOptions)}.{nameof(PostgreSqlStorageOptions.EnableTransactionScopeEnlistment)} to `true`.");
43 | }
44 |
45 | return _connectionStringBuilder = builder;
46 | }
47 | catch (ArgumentException ex)
48 | {
49 | throw new ArgumentException($"Connection string is not valid", nameof(connectionString), ex);
50 | }
51 | }
52 |
53 | ///
54 | public abstract NpgsqlConnection GetOrCreateConnection();
55 | }
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Hangfire.PostgreSql.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | PostgreSql storage implementation for Hangfire (background job system for ASP.NET and aspnet core applications).
5 | Copyright © 2014-2024 Frank Hommers and others
6 | Hangfire PostgreSql Storage
7 | 1.9.4
8 | Frank Hommers, Vytautas Kasparavičius, Žygimantas Arūna
9 | netstandard2.0
10 | Hangfire.PostgreSql
11 | Library
12 | Hangfire;PostgreSql;Postgres
13 | Hangfire.PostgreSql
14 | https://github.com/frankhommers/Hangfire.PostgreSql/releases
15 | http://hmm.rs/Hangfire.PostgreSql
16 |
17 | 1.9.4.0
18 | 1.9.4.0
19 | 1.9.4.0
20 | True
21 | LICENSE.md
22 | https://github.com/frankhommers/Hangfire.PostgreSql
23 | git
24 | https://github.com/hangfire-postgres
25 | default
26 | true
27 | $(NoWarn);1591
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | all
38 | runtime; build; native; contentfiles; analyzers; buildtransitive
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | True
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/IConnectionFactory.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.PostgreSql.
2 | // Copyright © 2014 Frank Hommers .
3 | //
4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.PostgreSql. If not, see .
16 | //
17 | // This work is based on the work of Sergey Odinokov, author of
18 | // Hangfire.
19 | //
20 | // Special thanks goes to him.
21 |
22 | using Npgsql;
23 |
24 | namespace Hangfire.PostgreSql
25 | {
26 | ///
27 | /// Connection factory for creating at runtime.
28 | ///
29 | public interface IConnectionFactory
30 | {
31 | ///
32 | /// Get or create .
33 | ///
34 | NpgsqlConnection GetOrCreateConnection();
35 | }
36 | }
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/IPersistentJobQueue.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.PostgreSql.
2 | // Copyright © 2014 Frank Hommers .
3 | //
4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.PostgreSql. If not, see .
16 | //
17 | // This work is based on the work of Sergey Odinokov, author of
18 | // Hangfire.
19 | //
20 | // Special thanks goes to him.
21 |
22 | using System.Data;
23 | using System.Threading;
24 | using Hangfire.Storage;
25 |
26 | namespace Hangfire.PostgreSql
27 | {
28 | public interface IPersistentJobQueue
29 | {
30 | IFetchedJob Dequeue(string[] queues, CancellationToken cancellationToken);
31 | void Enqueue(IDbConnection connection, string queue, string jobId);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/IPersistentJobQueueMonitoringApi.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.PostgreSql.
2 | // Copyright © 2014 Frank Hommers .
3 | //
4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.PostgreSql. If not, see .
16 | //
17 | // This work is based on the work of Sergey Odinokov, author of
18 | // Hangfire.
19 | //
20 | // Special thanks goes to him.
21 |
22 | using System.Collections.Generic;
23 |
24 | namespace Hangfire.PostgreSql
25 | {
26 | public interface IPersistentJobQueueMonitoringApi
27 | {
28 | IEnumerable GetQueues();
29 | IEnumerable GetEnqueuedJobIds(string queue, int from, int perPage);
30 | IEnumerable GetFetchedJobIds(string queue, int from, int perPage);
31 | EnqueuedAndFetchedCountDto GetEnqueuedAndFetchedCount(string queue);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/IPersistentJobQueueProvider.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.PostgreSql.
2 | // Copyright © 2014 Frank Hommers .
3 | //
4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.PostgreSql. If not, see .
16 | //
17 | // This work is based on the work of Sergey Odinokov, author of
18 | // Hangfire.
19 | //
20 | // Special thanks goes to him.
21 |
22 | namespace Hangfire.PostgreSql
23 | {
24 | public interface IPersistentJobQueueProvider
25 | {
26 | IPersistentJobQueue GetJobQueue();
27 | IPersistentJobQueueMonitoringApi GetJobQueueMonitoringApi();
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/JsonParameter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Data;
3 | using System.Text.Json;
4 | using Dapper;
5 | using Hangfire.Annotations;
6 | using Npgsql;
7 | using NpgsqlTypes;
8 |
9 | namespace Hangfire.PostgreSql;
10 |
11 | internal class JsonParameter : SqlMapper.ICustomQueryParameter
12 | {
13 | [CanBeNull] private readonly object _value;
14 | private readonly ValueType _type;
15 |
16 | public JsonParameter([CanBeNull] object value) : this(value, ValueType.Object)
17 | {
18 | }
19 |
20 | public JsonParameter([CanBeNull] object value, ValueType type)
21 | {
22 | _value = value;
23 | _type = type;
24 | }
25 |
26 | public void AddParameter(IDbCommand command, string name)
27 | {
28 | string value = _value switch {
29 | string { Length: > 0 } stringValue => stringValue,
30 | string { Length: 0 } or null => GetDefaultValue(),
31 | var _ => JsonSerializer.Serialize(_value),
32 | };
33 | command.Parameters.Add(new NpgsqlParameter(name, NpgsqlDbType.Jsonb) { Value = value });
34 | }
35 |
36 | private string GetDefaultValue()
37 | {
38 | return _type switch
39 | {
40 | ValueType.Object => "{}",
41 | ValueType.Array => "[]",
42 | var _ => throw new ArgumentOutOfRangeException(),
43 | };
44 | }
45 |
46 | public enum ValueType
47 | {
48 | Object,
49 | Array,
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/PersistentJobQueueProviderCollection.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.PostgreSql.
2 | // Copyright © 2014 Frank Hommers .
3 | //
4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.PostgreSql. If not, see .
16 | //
17 | // This work is based on the work of Sergey Odinokov, author of
18 | // Hangfire.
19 | //
20 | // Special thanks goes to him.
21 |
22 | using System;
23 | using System.Collections;
24 | using System.Collections.Generic;
25 |
26 | namespace Hangfire.PostgreSql
27 | {
28 | public class PersistentJobQueueProviderCollection : IEnumerable
29 | {
30 | private readonly IPersistentJobQueueProvider _defaultProvider;
31 |
32 | private readonly List _providers = new();
33 |
34 | private readonly Dictionary _providersByQueue = new(StringComparer.OrdinalIgnoreCase);
35 |
36 | public PersistentJobQueueProviderCollection(IPersistentJobQueueProvider defaultProvider)
37 | {
38 | _defaultProvider = defaultProvider ?? throw new ArgumentNullException(nameof(defaultProvider));
39 | _providers.Add(_defaultProvider);
40 | }
41 |
42 | public IEnumerator GetEnumerator()
43 | {
44 | return _providers.GetEnumerator();
45 | }
46 |
47 | IEnumerator IEnumerable.GetEnumerator()
48 | {
49 | return GetEnumerator();
50 | }
51 |
52 | public void Add(IPersistentJobQueueProvider provider, IEnumerable queues)
53 | {
54 | if (provider == null)
55 | {
56 | throw new ArgumentNullException(nameof(provider));
57 | }
58 |
59 | if (queues == null)
60 | {
61 | throw new ArgumentNullException(nameof(queues));
62 | }
63 |
64 | _providers.Add(provider);
65 |
66 | foreach (string queue in queues)
67 | {
68 | _providersByQueue.Add(queue, provider);
69 | }
70 | }
71 |
72 | public IPersistentJobQueueProvider GetProvider(string queue)
73 | {
74 | return _providersByQueue.TryGetValue(queue, out IPersistentJobQueueProvider provider)
75 | ? provider
76 | : _defaultProvider;
77 | }
78 |
79 | public void Remove(string queue)
80 | {
81 | if (!_providersByQueue.ContainsKey(queue))
82 | {
83 | return;
84 | }
85 |
86 | IPersistentJobQueueProvider provider = _providersByQueue[queue];
87 | _providersByQueue.Remove(queue);
88 | _providers.Remove(provider);
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/PostgreSqlBootstrapperConfigurationExtensions.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.PostgreSql.
2 | // Copyright © 2014 Frank Hommers .
3 | //
4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.PostgreSql. If not, see .
16 | //
17 | // This work is based on the work of Sergey Odinokov, author of
18 | // Hangfire.
19 | //
20 | // Special thanks goes to him.
21 |
22 | using System;
23 | using Npgsql;
24 |
25 | namespace Hangfire.PostgreSql
26 | {
27 | public static class PostgreSqlBootstrapperConfigurationExtensions
28 | {
29 | ///
30 | /// Tells the bootstrapper to use PostgreSQL as a job storage,
31 | /// that can be accessed using the given connection string or
32 | /// its name.
33 | ///
34 | /// Configuration
35 | /// Connection string
36 | [Obsolete("Will be removed in 2.0. Please use UsePostgreSqlStorage(Action) overload.")]
37 | public static IGlobalConfiguration UsePostgreSqlStorage(
38 | this IGlobalConfiguration configuration,
39 | string connectionString)
40 | {
41 | return configuration.UsePostgreSqlStorage(connectionString, null, new PostgreSqlStorageOptions());
42 | }
43 |
44 | ///
45 | /// Tells the bootstrapper to use PostgreSQL as a job storage
46 | /// with the given options, that can be accessed using the specified
47 | /// connection string.
48 | ///
49 | /// Configuration
50 | /// Connection string
51 | /// Advanced options
52 | [Obsolete("Will be removed in 2.0. Please use UsePostgreSqlStorage(Action, PostgreSqlStorageOptions) overload.")]
53 | public static IGlobalConfiguration UsePostgreSqlStorage(
54 | this IGlobalConfiguration configuration,
55 | string connectionString,
56 | PostgreSqlStorageOptions options)
57 | {
58 | return configuration.UsePostgreSqlStorage(connectionString, null, options);
59 | }
60 |
61 | ///
62 | /// Tells the bootstrapper to use PostgreSQL as a job storage
63 | /// with the given options, that can be accessed using the specified
64 | /// connection string.
65 | ///
66 | /// Configuration
67 | /// Connection string
68 | /// Optional setup action to apply to created connections
69 | /// Advanced options
70 | [Obsolete("Will be removed in 2.0. Please use UsePostgreSqlStorage(Action, PostgreSqlStorageOptions) overload.")]
71 | public static IGlobalConfiguration UsePostgreSqlStorage(
72 | this IGlobalConfiguration configuration,
73 | string connectionString,
74 | Action connectionSetup,
75 | PostgreSqlStorageOptions options)
76 | {
77 | return configuration.UsePostgreSqlStorage(configure => configure.UseNpgsqlConnection(connectionString, connectionSetup), options);
78 | }
79 |
80 | ///
81 | /// Tells the bootstrapper to use PostgreSQL as a job storage
82 | /// with the given options, that can be accessed using the specified
83 | /// connection factory.
84 | ///
85 | /// Configuration
86 | /// Connection factory
87 | /// Advanced options
88 | [Obsolete("Will be removed in 2.0. Please use UsePostgreSqlStorage(Action, PostgreSqlStorageOptions) overload.")]
89 | public static IGlobalConfiguration UsePostgreSqlStorage(
90 | this IGlobalConfiguration configuration,
91 | IConnectionFactory connectionFactory,
92 | PostgreSqlStorageOptions options)
93 | {
94 | return configuration.UsePostgreSqlStorage(configure => configure.UseConnectionFactory(connectionFactory), options);
95 | }
96 |
97 | ///
98 | /// Tells the bootstrapper to use PostgreSQL as a job storage
99 | /// with the given options, that can be accessed using the specified
100 | /// connection factory.
101 | ///
102 | /// Configuration
103 | /// Connection factory
104 | [Obsolete("Will be removed in 2.0. Please use UsePostgreSqlStorage(Action) overload.")]
105 | public static IGlobalConfiguration UsePostgreSqlStorage(
106 | this IGlobalConfiguration configuration,
107 | IConnectionFactory connectionFactory)
108 | {
109 | return configuration.UsePostgreSqlStorage(connectionFactory, new PostgreSqlStorageOptions());
110 | }
111 |
112 | ///
113 | /// Tells the bootstrapper to use PostgreSQL as the job storage with the default storage options.
114 | ///
115 | /// Configuration instance.
116 | /// Bootstrapper configuration action.
117 | /// instance whose generic type argument is .
118 | public static IGlobalConfiguration UsePostgreSqlStorage(this IGlobalConfiguration configuration, Action configure)
119 | {
120 | return configuration.UsePostgreSqlStorage(configure, new PostgreSqlStorageOptions());
121 | }
122 |
123 | ///
124 | /// Tells the bootstrapper to use PostgreSQL as the job storage with the specified storage options.
125 | ///
126 | /// Configuration instance.
127 | /// Bootstrapper configuration action.
128 | /// Storage options.
129 | /// instance whose generic type argument is .
130 | /// Throws if is not set up in the action.
131 | public static IGlobalConfiguration UsePostgreSqlStorage(this IGlobalConfiguration configuration, Action configure, PostgreSqlStorageOptions options)
132 | {
133 | if (options == null)
134 | {
135 | throw new ArgumentNullException(nameof(options));
136 | }
137 |
138 | PostgreSqlBootstrapperOptions bootstrapperOptions = new(options);
139 | configure(bootstrapperOptions);
140 |
141 | IConnectionFactory connectionFactory = bootstrapperOptions.ConnectionFactory;
142 | if (connectionFactory == null)
143 | {
144 | throw new InvalidOperationException("Connection factory is not specified");
145 | }
146 |
147 | PostgreSqlStorage storage = new(connectionFactory, options);
148 | return configuration.UseStorage(storage);
149 | }
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/PostgreSqlBootstrapperOptions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Hangfire.Annotations;
3 | using Hangfire.PostgreSql.Factories;
4 | using Npgsql;
5 |
6 | namespace Hangfire.PostgreSql;
7 |
8 | ///
9 | /// Bootstrapper options.
10 | ///
11 | public class PostgreSqlBootstrapperOptions
12 | {
13 | private readonly PostgreSqlStorageOptions _options;
14 |
15 | internal PostgreSqlBootstrapperOptions(PostgreSqlStorageOptions options)
16 | {
17 | _options = options ?? throw new ArgumentNullException(nameof(options));
18 | }
19 |
20 | [CanBeNull] internal IConnectionFactory ConnectionFactory { get; private set; }
21 |
22 | ///
23 | /// Configures the bootstrapper to use a custom to use for each database action.
24 | ///
25 | /// Instance of .
26 | /// This instance.
27 | /// Throws if is null.
28 | public PostgreSqlBootstrapperOptions UseConnectionFactory(IConnectionFactory connectionFactory)
29 | {
30 | ConnectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
31 | return this;
32 | }
33 |
34 | ///
35 | /// Configures the bootstrapper to create a new for each database action.
36 | ///
37 | /// Connection string.
38 | /// Optional additional connection setup action to be performed on the created .
39 | /// This instance.
40 | public PostgreSqlBootstrapperOptions UseNpgsqlConnection(string connectionString, [CanBeNull] Action connectionSetup = null)
41 | {
42 | return UseConnectionFactory(new NpgsqlConnectionFactory(connectionString, _options, connectionSetup));
43 | }
44 |
45 | public PostgreSqlBootstrapperOptions UseNpgsqlConnection(Func getConnectionString, [CanBeNull] Action connectionSetup = null)
46 | {
47 | return UseConnectionFactory(new NpgsqlConnectionFactory(getConnectionString, _options, connectionSetup));
48 | }
49 |
50 | ///
51 | /// Configures the bootstrapper to use the existing for each database action.
52 | ///
53 | /// to use.
54 | /// This instance.
55 | public PostgreSqlBootstrapperOptions UseExistingNpgsqlConnection(NpgsqlConnection connection)
56 | {
57 | return UseConnectionFactory(new ExistingNpgsqlConnectionFactory(connection, _options));
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/PostgreSqlDistributedLock.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.PostgreSql.
2 | // Copyright © 2014 Frank Hommers .
3 | //
4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.PostgreSql. If not, see .
16 | //
17 | // This work is based on the work of Sergey Odinokov, author of
18 | // Hangfire.
19 | //
20 | // Special thanks goes to him.
21 |
22 | using System;
23 | using System.Data;
24 | using System.Diagnostics;
25 | using System.Threading;
26 | using System.Transactions;
27 | using Dapper;
28 | using Hangfire.Annotations;
29 | using Hangfire.Logging;
30 | using Npgsql;
31 | using IsolationLevel = System.Data.IsolationLevel;
32 |
33 | namespace Hangfire.PostgreSql
34 | {
35 | public sealed class PostgreSqlDistributedLock
36 | {
37 | private static readonly ILog _logger = LogProvider.GetCurrentClassLogger();
38 |
39 | private static void Log(string resource, string message, Exception ex)
40 | {
41 | bool isConcurrencyError = ex is PostgresException { SqlState: PostgresErrorCodes.SerializationFailure };
42 | _logger.Log(isConcurrencyError ? LogLevel.Trace : LogLevel.Warn, () => $"{resource}: {message}", ex);
43 | }
44 |
45 | internal static void Acquire(IDbConnection connection, string resource, TimeSpan timeout, PostgreSqlStorageOptions options)
46 | {
47 | if (connection == null)
48 | {
49 | throw new ArgumentNullException(nameof(connection));
50 | }
51 |
52 | if (string.IsNullOrEmpty(resource))
53 | {
54 | throw new ArgumentNullException(nameof(resource));
55 | }
56 |
57 | if (options == null)
58 | {
59 | throw new ArgumentNullException(nameof(options));
60 | }
61 |
62 | if (connection.State != ConnectionState.Open)
63 | {
64 | // When we are passing a closed connection to Dapper's Execute method,
65 | // it kindly opens it for us, but after command execution, it will be closed
66 | // automatically, and our just-acquired application lock will immediately
67 | // be released. This is not behavior we want to achieve, so let's throw an
68 | // exception instead.
69 | throw new InvalidOperationException("Connection must be open before acquiring a distributed lock.");
70 | }
71 |
72 | LockHandler.Lock(resource, timeout, connection, options);
73 | }
74 |
75 | internal static void Release(IDbConnection connection, string resource, PostgreSqlStorageOptions options)
76 | {
77 | if (connection == null)
78 | {
79 | throw new ArgumentNullException(nameof(connection));
80 | }
81 |
82 | if (resource == null)
83 | {
84 | throw new ArgumentNullException(nameof(resource));
85 | }
86 |
87 | if (options == null)
88 | {
89 | throw new ArgumentNullException(nameof(options));
90 | }
91 |
92 | if (!LockHandler.TryRemoveLock(resource, connection, options, false))
93 | {
94 | throw new PostgreSqlDistributedLockException(resource);
95 | }
96 | }
97 |
98 | private static class LockHandler
99 | {
100 | public static void Lock(string resource, TimeSpan timeout, IDbConnection connection, PostgreSqlStorageOptions options)
101 | {
102 | Stopwatch lockAcquiringTime = Stopwatch.StartNew();
103 |
104 | bool tryAcquireLock = true;
105 | Func tryLock = options.UseNativeDatabaseTransactions
106 | ? TransactionLockHandler.TryLock
107 | : UpdateCountLockHandler.TryLock;
108 |
109 | while (tryAcquireLock)
110 | {
111 | if (connection.State != ConnectionState.Open)
112 | {
113 | connection.Open();
114 | }
115 |
116 | TryRemoveLock(resource, connection, options, true);
117 |
118 | try
119 | {
120 | if (tryLock(connection, options.SchemaName, resource))
121 | {
122 | return;
123 | }
124 | }
125 | catch (Exception ex)
126 | {
127 | Log(resource, "Failed to acquire lock", ex);
128 | }
129 |
130 | if (lockAcquiringTime.ElapsedMilliseconds > timeout.TotalMilliseconds)
131 | {
132 | tryAcquireLock = false;
133 | }
134 | else
135 | {
136 | int sleepDuration = (int)(timeout.TotalMilliseconds - lockAcquiringTime.ElapsedMilliseconds);
137 | if (sleepDuration > 1000)
138 | {
139 | sleepDuration = 1000;
140 | }
141 |
142 | if (sleepDuration > 0)
143 | {
144 | Thread.Sleep(sleepDuration);
145 | }
146 | else
147 | {
148 | tryAcquireLock = false;
149 | }
150 | }
151 | }
152 |
153 | throw new PostgreSqlDistributedLockException(resource);
154 | }
155 |
156 | public static bool TryRemoveLock(string resource, IDbConnection connection, PostgreSqlStorageOptions options, bool onlyExpired)
157 | {
158 | IDbTransaction trx = null;
159 | try
160 | {
161 | // Non-expired locks are removed only when releasing them. Transaction is not needed in that case.
162 | if (onlyExpired && options.UseNativeDatabaseTransactions)
163 | {
164 | trx = TransactionLockHandler.BeginTransactionIfNotPresent(connection);
165 | }
166 |
167 | DateTime timeout = onlyExpired ? DateTime.UtcNow - options.DistributedLockTimeout : DateTime.MaxValue;
168 |
169 | int rowsAffected = connection.Execute($@"DELETE FROM ""{options.SchemaName}"".""lock"" WHERE ""resource"" = @Resource AND ""acquired"" < @Timeout",
170 | new {
171 | Resource = resource,
172 | Timeout = timeout,
173 | }, trx);
174 |
175 | trx?.Commit();
176 |
177 | return rowsAffected >= 0;
178 | }
179 | catch (Exception ex)
180 | {
181 | Log(resource, "Failed to remove lock", ex);
182 | return false;
183 | }
184 | finally
185 | {
186 | trx?.Dispose();
187 | }
188 | }
189 | }
190 |
191 | private static class TransactionLockHandler
192 | {
193 | public static bool TryLock(IDbConnection connection, string schemaName, string resource)
194 | {
195 | IDbTransaction trx = null;
196 | try
197 | {
198 | trx = BeginTransactionIfNotPresent(connection);
199 |
200 | int rowsAffected = connection.Execute($@"
201 | INSERT INTO ""{schemaName}"".""lock""(""resource"", ""acquired"")
202 | SELECT @Resource, @Acquired
203 | WHERE NOT EXISTS (
204 | SELECT 1 FROM ""{schemaName}"".""lock""
205 | WHERE ""resource"" = @Resource
206 | )
207 | ON CONFLICT DO NOTHING;
208 | ",
209 | new {
210 | Resource = resource,
211 | Acquired = DateTime.UtcNow,
212 | }, trx);
213 | trx?.Commit();
214 |
215 | return rowsAffected > 0;
216 | }
217 | finally
218 | {
219 | trx?.Dispose();
220 | }
221 | }
222 |
223 | [CanBeNull]
224 | public static IDbTransaction BeginTransactionIfNotPresent(IDbConnection connection)
225 | {
226 | // If transaction scope was created outside of hangfire, the newly-opened connection is automatically enlisted into the transaction.
227 | // Starting a new transaction throws "A transaction is already in progress; nested/concurrent transactions aren't supported." in that case.
228 | return Transaction.Current == null ? connection.BeginTransaction(IsolationLevel.ReadCommitted) : null;
229 | }
230 | }
231 |
232 | private static class UpdateCountLockHandler
233 | {
234 | public static bool TryLock(IDbConnection connection, string schemaName, string resource)
235 | {
236 | connection.Execute($@"
237 | INSERT INTO ""{schemaName}"".""lock""(""resource"", ""updatecount"", ""acquired"")
238 | SELECT @Resource, 0, @Acquired
239 | WHERE NOT EXISTS (
240 | SELECT 1 FROM ""{schemaName}"".""lock""
241 | WHERE ""resource"" = @Resource
242 | )
243 | ON CONFLICT DO NOTHING;
244 | ", new {
245 | Resource = resource,
246 | Acquired = DateTime.UtcNow,
247 | });
248 |
249 | int rowsAffected = connection.Execute(
250 | $@"UPDATE ""{schemaName}"".""lock"" SET ""updatecount"" = 1 WHERE ""updatecount"" = 0 AND ""resource"" = @Resource",
251 | new { Resource = resource });
252 |
253 | return rowsAffected > 0;
254 | }
255 | }
256 | }
257 | }
258 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/PostgreSqlDistributedLockException.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.PostgreSql.
2 | // Copyright © 2014 Frank Hommers .
3 | //
4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.PostgreSql. If not, see .
16 | //
17 | // This work is based on the work of Sergey Odinokov, author of
18 | // Hangfire.
19 | //
20 | // Special thanks goes to him.
21 |
22 | using System;
23 | using Hangfire.Storage;
24 |
25 | namespace Hangfire.PostgreSql
26 | {
27 | [Serializable]
28 | public class PostgreSqlDistributedLockException : DistributedLockTimeoutException
29 | {
30 | public PostgreSqlDistributedLockException(string resource) : base(resource)
31 | {
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/PostgreSqlFetchedJob.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.PostgreSql.
2 | // Copyright © 2014 Frank Hommers .
3 | //
4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.PostgreSql. If not, see .
16 | //
17 | // This work is based on the work of Sergey Odinokov, author of
18 | // Hangfire.
19 | //
20 | // Special thanks goes to him.
21 |
22 | using System;
23 | using System.Threading;
24 | using Dapper;
25 | using Hangfire.Logging;
26 | using Hangfire.PostgreSql.Utils;
27 | using Hangfire.Storage;
28 |
29 | namespace Hangfire.PostgreSql
30 | {
31 | public class PostgreSqlFetchedJob : IFetchedJob
32 | {
33 | private readonly ILog _logger = LogProvider.GetLogger(typeof(PostgreSqlFetchedJob));
34 |
35 | private readonly PostgreSqlStorage _storage;
36 | private bool _disposed;
37 | private bool _removedFromQueue;
38 | private bool _requeued;
39 |
40 | private readonly object _syncRoot = new object();
41 | private long _lastHeartbeat;
42 | private readonly TimeSpan _interval;
43 |
44 | public PostgreSqlFetchedJob(
45 | PostgreSqlStorage storage,
46 | long id,
47 | string jobId,
48 | string queue,
49 | DateTime? fetchedAt)
50 | {
51 | _storage = storage ?? throw new ArgumentNullException(nameof(storage));
52 |
53 | Id = id;
54 | JobId = jobId ?? throw new ArgumentNullException(nameof(jobId));
55 | Queue = queue ?? throw new ArgumentNullException(nameof(queue));
56 | FetchedAt = fetchedAt ?? throw new ArgumentNullException(nameof(fetchedAt));
57 |
58 | if (storage.Options.UseSlidingInvisibilityTimeout)
59 | {
60 | _lastHeartbeat = TimestampHelper.GetTimestamp();
61 | _interval = TimeSpan.FromSeconds(storage.Options.InvisibilityTimeout.TotalSeconds / 5);
62 | storage.HeartbeatProcess.Track(this);
63 | }
64 | }
65 |
66 | public long Id { get; }
67 | public string Queue { get; }
68 | public string JobId { get; }
69 | internal DateTime? FetchedAt { get; private set; }
70 |
71 | public void RemoveFromQueue()
72 | {
73 | lock (_syncRoot)
74 | {
75 | if (!FetchedAt.HasValue)
76 | {
77 | return;
78 | }
79 |
80 | _storage.UseConnection(null, connection => connection.Execute($@"
81 | DELETE FROM ""{_storage.Options.SchemaName}"".""jobqueue"" WHERE ""id"" = @Id AND ""fetchedat"" = @FetchedAt;
82 | ",
83 | new { Id, FetchedAt }));
84 |
85 | _removedFromQueue = true;
86 | }
87 | }
88 |
89 | public void Requeue()
90 | {
91 | lock (_syncRoot)
92 | {
93 | if (!FetchedAt.HasValue)
94 | {
95 | return;
96 | }
97 |
98 | _storage.UseConnection(null, connection => connection.Execute($@"
99 | UPDATE ""{_storage.Options.SchemaName}"".""jobqueue""
100 | SET ""fetchedat"" = NULL
101 | WHERE ""id"" = @Id AND ""fetchedat"" = @FetchedAt;
102 | ",
103 | new { Id, FetchedAt }));
104 |
105 | FetchedAt = null;
106 | _requeued = true;
107 | }
108 | }
109 |
110 | public void Dispose()
111 | {
112 | if (_disposed)
113 | {
114 | return;
115 | }
116 |
117 | _disposed = true;
118 |
119 | DisposeTimer();
120 |
121 | lock (_syncRoot)
122 | {
123 | if (!_removedFromQueue && !_requeued)
124 | {
125 | Requeue();
126 | }
127 | }
128 | }
129 |
130 | internal void DisposeTimer()
131 | {
132 | if (_storage.Options.UseSlidingInvisibilityTimeout)
133 | {
134 | _storage.HeartbeatProcess.Untrack(this);
135 | }
136 | }
137 |
138 | internal void ExecuteKeepAliveQueryIfRequired()
139 | {
140 | var now = TimestampHelper.GetTimestamp();
141 |
142 | if (TimestampHelper.Elapsed(now, Interlocked.Read(ref _lastHeartbeat)) < _interval)
143 | {
144 | return;
145 | }
146 |
147 | lock (_syncRoot)
148 | {
149 | if (!FetchedAt.HasValue)
150 | {
151 | return;
152 | }
153 |
154 | if (_requeued || _removedFromQueue)
155 | {
156 | return;
157 | }
158 |
159 | string updateFetchAtSql = $@"
160 | UPDATE ""{_storage.Options.SchemaName}"".""jobqueue""
161 | SET ""fetchedat"" = NOW()
162 | WHERE ""id"" = @id AND ""fetchedat"" = @fetchedAt
163 | RETURNING ""fetchedat"" AS ""FetchedAt"";
164 | ";
165 |
166 | try
167 | {
168 | _storage.UseConnection(null, connection =>
169 | {
170 | FetchedAt = connection.ExecuteScalar(updateFetchAtSql,
171 | new { queue = Queue, id = Id, fetchedAt = FetchedAt });
172 | });
173 |
174 | if (!FetchedAt.HasValue)
175 | {
176 | _logger.Warn(
177 | $"Background job identifier '{JobId}' was fetched by another worker, will not execute keep alive.");
178 | }
179 |
180 | _logger.Trace($"Keep-alive query for message {Id} sent");
181 | Interlocked.Exchange(ref _lastHeartbeat, now);
182 | }
183 | catch (Exception ex) when (ex.IsCatchableExceptionType())
184 | {
185 | _logger.DebugException($"Unable to execute keep-alive query for message {Id}", ex);
186 | }
187 | }
188 | }
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/PostgreSqlHeartbeatProcess.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.PostgreSql.
2 | // Copyright © 2014 Frank Hommers .
3 | //
4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.PostgreSql. If not, see .
16 | //
17 | // This work is based on the work of Sergey Odinokov, author of
18 | // Hangfire.
19 | //
20 | // Special thanks goes to him.
21 |
22 | using System;
23 | using System.Collections.Concurrent;
24 | using System.Threading;
25 | using Hangfire.Common;
26 | using Hangfire.Server;
27 |
28 | namespace Hangfire.PostgreSql
29 | {
30 | #pragma warning disable CS0618
31 | internal sealed class PostgreSqlHeartbeatProcess : IServerComponent, IBackgroundProcess
32 | #pragma warning restore CS0618
33 | {
34 | private readonly ConcurrentDictionary _items = new();
35 |
36 | public void Track(PostgreSqlFetchedJob item)
37 | {
38 | _items.TryAdd(item, null);
39 | }
40 |
41 | public void Untrack(PostgreSqlFetchedJob item)
42 | {
43 | _items.TryRemove(item, out var _);
44 | }
45 |
46 | public void Execute(CancellationToken cancellationToken)
47 | {
48 | foreach (var item in _items)
49 | {
50 | item.Key.ExecuteKeepAliveQueryIfRequired();
51 | }
52 |
53 | cancellationToken.Wait(TimeSpan.FromSeconds(1));
54 | }
55 |
56 | public void Execute(BackgroundProcessContext context)
57 | {
58 | Execute(context.StoppingToken);
59 | }
60 | }
61 | }
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/PostgreSqlJobQueueMonitoringApi.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.PostgreSql.
2 | // Copyright © 2014 Frank Hommers .
3 | //
4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.PostgreSql. If not, see .
16 | //
17 | // This work is based on the work of Sergey Odinokov, author of
18 | // Hangfire.
19 | //
20 | // Special thanks goes to him.
21 |
22 | using System;
23 | using System.Collections.Generic;
24 | using System.Linq;
25 | using Dapper;
26 |
27 | namespace Hangfire.PostgreSql
28 | {
29 | internal class PostgreSqlJobQueueMonitoringApi : IPersistentJobQueueMonitoringApi
30 | {
31 | private readonly PostgreSqlStorage _storage;
32 |
33 | public PostgreSqlJobQueueMonitoringApi(PostgreSqlStorage storage)
34 | {
35 | _storage = storage ?? throw new ArgumentNullException(nameof(storage));
36 | }
37 |
38 | public IEnumerable GetQueues()
39 | {
40 | string sqlQuery = $@"SELECT DISTINCT ""queue"" FROM ""{_storage.Options.SchemaName}"".""jobqueue""";
41 | return _storage.UseConnection(null, connection => connection.Query(sqlQuery).ToList());
42 | }
43 |
44 | public IEnumerable GetEnqueuedJobIds(string queue, int from, int perPage)
45 | {
46 | return GetQueuedOrFetchedJobIds(queue, false, from, perPage);
47 | }
48 |
49 | public IEnumerable GetFetchedJobIds(string queue, int from, int perPage)
50 | {
51 | return GetQueuedOrFetchedJobIds(queue, true, from, perPage);
52 | }
53 |
54 | public EnqueuedAndFetchedCountDto GetEnqueuedAndFetchedCount(string queue)
55 | {
56 | string sqlQuery = $@"
57 | SELECT (
58 | SELECT COUNT(*)
59 | FROM ""{_storage.Options.SchemaName}"".""jobqueue""
60 | WHERE ""fetchedat"" IS NULL
61 | AND ""queue"" = @Queue
62 | ) ""EnqueuedCount"",
63 | (
64 | SELECT COUNT(*)
65 | FROM ""{_storage.Options.SchemaName}"".""jobqueue""
66 | WHERE ""fetchedat"" IS NOT NULL
67 | AND ""queue"" = @Queue
68 | ) ""FetchedCount"";
69 | ";
70 |
71 | (long enqueuedCount, long fetchedCount) = _storage.UseConnection(null, connection =>
72 | connection.QuerySingle<(long EnqueuedCount, long FetchedCount)>(sqlQuery, new { Queue = queue }));
73 |
74 | return new EnqueuedAndFetchedCountDto {
75 | EnqueuedCount = enqueuedCount,
76 | FetchedCount = fetchedCount,
77 | };
78 | }
79 |
80 | private IEnumerable GetQueuedOrFetchedJobIds(string queue, bool fetched, int from, int perPage)
81 | {
82 | string sqlQuery = $@"
83 | SELECT j.""id""
84 | FROM ""{_storage.Options.SchemaName}"".""jobqueue"" jq
85 | LEFT JOIN ""{_storage.Options.SchemaName}"".""job"" j ON jq.""jobid"" = j.""id""
86 | WHERE jq.""queue"" = @Queue
87 | AND jq.""fetchedat"" {(fetched ? "IS NOT NULL" : "IS NULL")}
88 | AND j.""id"" IS NOT NULL
89 | ORDER BY jq.""fetchedat"", jq.""jobid""
90 | LIMIT @Limit OFFSET @Offset;
91 | ";
92 |
93 | return _storage.UseConnection(null, connection => connection.Query(sqlQuery,
94 | new { Queue = queue, Offset = from, Limit = perPage })
95 | .ToList());
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/PostgreSqlJobQueueProvider.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.PostgreSql.
2 | // Copyright © 2014 Frank Hommers .
3 | //
4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.PostgreSql. If not, see .
16 | //
17 | // This work is based on the work of Sergey Odinokov, author of
18 | // Hangfire.
19 | //
20 | // Special thanks goes to him.
21 |
22 | using System;
23 |
24 | namespace Hangfire.PostgreSql
25 | {
26 | public class PostgreSqlJobQueueProvider : IPersistentJobQueueProvider
27 | {
28 | public PostgreSqlJobQueueProvider(PostgreSqlStorage storage, PostgreSqlStorageOptions options)
29 | {
30 | Storage = storage ?? throw new ArgumentNullException(nameof(storage));
31 | Options = options ?? throw new ArgumentNullException(nameof(options));
32 | }
33 |
34 | public PostgreSqlStorageOptions Options { get; }
35 | public PostgreSqlStorage Storage { get; }
36 |
37 | public IPersistentJobQueue GetJobQueue()
38 | {
39 | return new PostgreSqlJobQueue(Storage);
40 | }
41 |
42 | public IPersistentJobQueueMonitoringApi GetJobQueueMonitoringApi()
43 | {
44 | return new PostgreSqlJobQueueMonitoringApi(Storage);
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/PostgreSqlObjectsInstaller.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.PostgreSql.
2 | // Copyright © 2014 Frank Hommers .
3 | //
4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.PostgreSql. If not, see .
16 | //
17 | // This work is based on the work of Sergey Odinokov, author of
18 | // Hangfire.
19 | //
20 | // Special thanks goes to him.
21 |
22 | using System;
23 | using System.Data;
24 | using System.Globalization;
25 | using System.IO;
26 | using System.Reflection;
27 | using System.Resources;
28 | using Hangfire.Logging;
29 | using Npgsql;
30 |
31 | namespace Hangfire.PostgreSql
32 | {
33 | public static class PostgreSqlObjectsInstaller
34 | {
35 | private static readonly ILog _logger = LogProvider.GetLogger(typeof(PostgreSqlStorage));
36 |
37 | public static void Install(NpgsqlConnection connection, string schemaName = "hangfire")
38 | {
39 | if (connection == null)
40 | {
41 | throw new ArgumentNullException(nameof(connection));
42 | }
43 |
44 | _logger.Info("Start installing Hangfire SQL objects...");
45 |
46 | // starts with version 3 to keep in check with Hangfire SqlServer, but I couldn't keep up with that idea after all;
47 | int version = 3;
48 | int previousVersion = 1;
49 | do
50 | {
51 | try
52 | {
53 | string script;
54 | try
55 | {
56 | script = GetStringResource(typeof(PostgreSqlObjectsInstaller).GetTypeInfo().Assembly,
57 | $"Hangfire.PostgreSql.Scripts.Install.v{version.ToString(CultureInfo.InvariantCulture)}.sql");
58 | }
59 | catch (MissingManifestResourceException)
60 | {
61 | break;
62 | }
63 |
64 | if (schemaName != "hangfire")
65 | {
66 | script = script.Replace("'hangfire'", $"'{schemaName}'").Replace(@"""hangfire""", $@"""{schemaName}""");
67 | }
68 |
69 | if (!VersionAlreadyApplied(connection, schemaName, version))
70 | {
71 | string commandText = $@"{script}; UPDATE ""{schemaName}"".""schema"" SET ""version"" = @Version WHERE ""version"" = @PreviousVersion";
72 | using NpgsqlTransaction transaction = connection.BeginTransaction(IsolationLevel.Serializable);
73 | using NpgsqlCommand command = new(commandText, connection, transaction);
74 | command.CommandTimeout = 120;
75 | command.Parameters.Add(new NpgsqlParameter("Version", version));
76 | command.Parameters.Add(new NpgsqlParameter("PreviousVersion", previousVersion));
77 | try
78 | {
79 | command.ExecuteNonQuery();
80 | transaction.Commit();
81 | }
82 | catch (PostgresException ex)
83 | {
84 | if ((ex.MessageText ?? "") != "version-already-applied")
85 | {
86 | throw;
87 | }
88 | }
89 | }
90 | }
91 | catch (Exception ex)
92 | {
93 | if (ex.Source.Equals("Npgsql"))
94 | {
95 | _logger.ErrorException("Error while executing install/upgrade", ex);
96 | }
97 |
98 | throw;
99 | }
100 |
101 | previousVersion = version;
102 | version++;
103 | } while (true);
104 |
105 | _logger.Info("Hangfire SQL objects installed.");
106 | }
107 |
108 | private static bool VersionAlreadyApplied(NpgsqlConnection connection, string schemaName, int version)
109 | {
110 | try
111 | {
112 | using NpgsqlCommand command = new($@"SELECT true ""VersionAlreadyApplied"" FROM ""{schemaName}"".""schema"" WHERE ""version"" >= $1", connection);
113 | command.Parameters.Add(new NpgsqlParameter { Value = version });
114 | object result = command.ExecuteScalar();
115 | if (true.Equals(result))
116 | {
117 | return true;
118 | }
119 | }
120 | catch (PostgresException ex)
121 | {
122 | if (ex.SqlState.Equals(PostgresErrorCodes.UndefinedTable)) //42P01: Relation (table) does not exist. So no schema table yet.
123 | {
124 | return false;
125 | }
126 |
127 | throw;
128 | }
129 |
130 | return false;
131 | }
132 |
133 | private static string GetStringResource(Assembly assembly, string resourceName)
134 | {
135 | using Stream stream = assembly.GetManifestResourceStream(resourceName);
136 | if (stream == null)
137 | {
138 | throw new MissingManifestResourceException($"Requested resource `{resourceName}` was not found in the assembly `{assembly}`.");
139 | }
140 |
141 | using StreamReader reader = new(stream);
142 | return reader.ReadToEnd();
143 | }
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/PostgreSqlStorageOptions.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.PostgreSql.
2 | // Copyright © 2014 Frank Hommers .
3 | //
4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.PostgreSql. If not, see .
16 | //
17 | // This work is based on the work of Sergey Odinokov, author of
18 | // Hangfire.
19 | //
20 | // Special thanks goes to him.
21 |
22 | using System;
23 |
24 | namespace Hangfire.PostgreSql
25 | {
26 | public class PostgreSqlStorageOptions
27 | {
28 | private static readonly TimeSpan _minimumQueuePollInterval = TimeSpan.FromMilliseconds(50);
29 |
30 | private int _deleteExpiredBatchSize;
31 | private TimeSpan _distributedLockTimeout;
32 | private TimeSpan _invisibilityTimeout;
33 | private TimeSpan _jobExpirationCheckInterval;
34 | private TimeSpan _queuePollInterval;
35 | private TimeSpan _transactionSerializationTimeout;
36 | private TimeSpan _countersAggregateInterval;
37 |
38 | public PostgreSqlStorageOptions()
39 | {
40 | QueuePollInterval = TimeSpan.FromSeconds(15);
41 | InvisibilityTimeout = TimeSpan.FromMinutes(30);
42 | DistributedLockTimeout = TimeSpan.FromMinutes(10);
43 | TransactionSynchronisationTimeout = TimeSpan.FromMilliseconds(500);
44 | JobExpirationCheckInterval = TimeSpan.FromHours(1);
45 | CountersAggregateInterval = TimeSpan.FromMinutes(5);
46 | SchemaName = "hangfire";
47 | AllowUnsafeValues = false;
48 | UseNativeDatabaseTransactions = true;
49 | PrepareSchemaIfNecessary = true;
50 | EnableTransactionScopeEnlistment = true;
51 | DeleteExpiredBatchSize = 1000;
52 | UseSlidingInvisibilityTimeout = false;
53 | }
54 |
55 | public TimeSpan QueuePollInterval
56 | {
57 | get => _queuePollInterval;
58 | set {
59 | ThrowIfValueIsLowerThan(_minimumQueuePollInterval, value, nameof(QueuePollInterval));
60 | _queuePollInterval = value;
61 | }
62 | }
63 |
64 | public TimeSpan InvisibilityTimeout
65 | {
66 | get => _invisibilityTimeout;
67 | set {
68 | ThrowIfValueIsNotPositive(value, nameof(InvisibilityTimeout));
69 | _invisibilityTimeout = value;
70 | }
71 | }
72 |
73 | public TimeSpan DistributedLockTimeout
74 | {
75 | get => _distributedLockTimeout;
76 | set {
77 | ThrowIfValueIsNotPositive(value, nameof(DistributedLockTimeout));
78 | _distributedLockTimeout = value;
79 | }
80 | }
81 |
82 | // ReSharper disable once IdentifierTypo
83 | public TimeSpan TransactionSynchronisationTimeout
84 | {
85 | get => _transactionSerializationTimeout;
86 | set {
87 | ThrowIfValueIsNotPositive(value, nameof(TransactionSynchronisationTimeout));
88 | _transactionSerializationTimeout = value;
89 | }
90 | }
91 |
92 | public TimeSpan JobExpirationCheckInterval
93 | {
94 | get => _jobExpirationCheckInterval;
95 | set {
96 | ThrowIfValueIsNotPositive(value, nameof(JobExpirationCheckInterval));
97 | _jobExpirationCheckInterval = value;
98 | }
99 | }
100 |
101 | public TimeSpan CountersAggregateInterval
102 | {
103 | get => _countersAggregateInterval;
104 | set {
105 | ThrowIfValueIsNotPositive(value, nameof(CountersAggregateInterval));
106 | _countersAggregateInterval = value;
107 | }
108 | }
109 |
110 | ///
111 | /// Gets or sets the number of records deleted in a single batch in expiration manager
112 | ///
113 | public int DeleteExpiredBatchSize
114 | {
115 | get => _deleteExpiredBatchSize;
116 | set {
117 | ThrowIfValueIsNotPositive(value, nameof(DeleteExpiredBatchSize));
118 | _deleteExpiredBatchSize = value;
119 | }
120 | }
121 |
122 | public bool AllowUnsafeValues { get; set; }
123 | public bool UseNativeDatabaseTransactions { get; set; }
124 | public bool PrepareSchemaIfNecessary { get; set; }
125 | public string SchemaName { get; set; }
126 | public bool EnableTransactionScopeEnlistment { get; set; }
127 | public bool EnableLongPolling { get; set; }
128 |
129 | ///
130 | /// Apply a sliding invisibility timeout where the last fetched time is continually updated in the background.
131 | /// This allows a lower invisibility timeout to be used with longer running jobs
132 | /// IMPORTANT: If option is used, then sliding invisiblity timeouts will not work
133 | /// since the background storage processes are not run (which is used to update the invisibility timeouts)
134 | ///
135 | public bool UseSlidingInvisibilityTimeout { get; set; }
136 |
137 | private static void ThrowIfValueIsNotPositive(TimeSpan value, string fieldName)
138 | {
139 | string message = $"The {fieldName} property value should be positive. Given: {value}.";
140 |
141 | if (value == TimeSpan.Zero)
142 | {
143 | throw new ArgumentException(message, nameof(value));
144 | }
145 |
146 | if (value != value.Duration())
147 | {
148 | throw new ArgumentException(message, nameof(value));
149 | }
150 | }
151 |
152 | private void ThrowIfValueIsLowerThan(TimeSpan minValue, TimeSpan value, string fieldName)
153 | {
154 | if (!AllowUnsafeValues)
155 | {
156 | string message = $"The {fieldName} property value seems to be too low ({value}, lower than suggested minimum of {minValue}). Consider increasing it. If you really need to have such a low value, please set {nameof(PostgreSqlStorageOptions)}.{nameof(AllowUnsafeValues)} to true.";
157 |
158 | if (value < minValue)
159 | {
160 | throw new ArgumentException(message, nameof(value));
161 | }
162 | }
163 |
164 | ThrowIfValueIsNotPositive(value, fieldName);
165 | }
166 |
167 | private static void ThrowIfValueIsNotPositive(int value, string fieldName)
168 | {
169 | if (value <= 0)
170 | {
171 | throw new ArgumentException($"The {fieldName} property value should be positive. Given: {value}.");
172 | }
173 | }
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 |
3 | [assembly: InternalsVisibleTo("Hangfire.PostgreSql.Tests")]
4 | // Allow the generation of mocks for internal types
5 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
6 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Scripts/Install.v10.sql:
--------------------------------------------------------------------------------
1 | SET search_path = 'hangfire';
2 | --
3 | -- Table structure for table `Schema`
4 | --
5 |
6 | DO
7 | $$
8 | BEGIN
9 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 10) THEN
10 | RAISE EXCEPTION 'version-already-applied';
11 | END IF;
12 | END
13 | $$;
14 |
15 | ALTER TABLE "jobqueue"
16 | ALTER COLUMN "queue" TYPE TEXT;
17 |
18 | RESET search_path;
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Scripts/Install.v11.sql:
--------------------------------------------------------------------------------
1 | SET search_path = 'hangfire';
2 |
3 | DO
4 | $$
5 | BEGIN
6 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 11) THEN
7 | RAISE EXCEPTION 'version-already-applied';
8 | END IF;
9 | END
10 | $$;
11 |
12 | ALTER TABLE "counter"
13 | ALTER COLUMN id TYPE BIGINT;
14 | ALTER TABLE "hash"
15 | ALTER COLUMN id TYPE BIGINT;
16 | ALTER TABLE "job"
17 | ALTER COLUMN id TYPE BIGINT;
18 | ALTER TABLE "job"
19 | ALTER COLUMN stateid TYPE BIGINT;
20 | ALTER TABLE "state"
21 | ALTER COLUMN id TYPE BIGINT;
22 | ALTER TABLE "state"
23 | ALTER COLUMN jobid TYPE BIGINT;
24 | ALTER TABLE "jobparameter"
25 | ALTER COLUMN id TYPE BIGINT;
26 | ALTER TABLE "jobparameter"
27 | ALTER COLUMN jobid TYPE BIGINT;
28 | ALTER TABLE "jobqueue"
29 | ALTER COLUMN id TYPE BIGINT;
30 | ALTER TABLE "jobqueue"
31 | ALTER COLUMN jobid TYPE BIGINT;
32 | ALTER TABLE "list"
33 | ALTER COLUMN id TYPE BIGINT;
34 | ALTER TABLE "set"
35 | ALTER COLUMN id TYPE BIGINT;
36 |
37 | RESET search_path;
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Scripts/Install.v12.sql:
--------------------------------------------------------------------------------
1 | SET search_path = 'hangfire';
2 |
3 | DO
4 | $$
5 | BEGIN
6 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 12) THEN
7 | RAISE EXCEPTION 'version-already-applied';
8 | END IF;
9 | END
10 | $$;
11 |
12 | ALTER TABLE "counter"
13 | ALTER COLUMN "key" TYPE TEXT;
14 | ALTER TABLE "hash"
15 | ALTER COLUMN "key" TYPE TEXT;
16 | ALTER TABLE "hash"
17 | ALTER COLUMN field TYPE TEXT;
18 | ALTER TABLE "job"
19 | ALTER COLUMN statename TYPE TEXT;
20 | ALTER TABLE "list"
21 | ALTER COLUMN "key" TYPE TEXT;
22 | ALTER TABLE "server"
23 | ALTER COLUMN id TYPE TEXT;
24 | ALTER TABLE "set"
25 | ALTER COLUMN "key" TYPE TEXT;
26 | ALTER TABLE "jobparameter"
27 | ALTER COLUMN "name" TYPE TEXT;
28 | ALTER TABLE "state"
29 | ALTER COLUMN "name" TYPE TEXT;
30 | ALTER TABLE "state"
31 | ALTER COLUMN reason TYPE TEXT;
32 |
33 | RESET search_path;
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Scripts/Install.v13.sql:
--------------------------------------------------------------------------------
1 | SET search_path = 'hangfire';
2 |
3 |
4 |
5 | DO
6 | $$
7 | BEGIN
8 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 13) THEN
9 | RAISE EXCEPTION 'version-already-applied';
10 | END IF;
11 | END
12 | $$;
13 |
14 | CREATE INDEX IF NOT EXISTS jobqueue_queue_fetchat_jobId ON jobqueue USING btree (queue asc, fetchedat asc nulls last, jobid asc);
15 |
16 | RESET search_path;
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Scripts/Install.v14.sql:
--------------------------------------------------------------------------------
1 | SET search_path = 'hangfire';
2 |
3 | DO
4 | $$
5 | BEGIN
6 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 14) THEN
7 | RAISE EXCEPTION 'version-already-applied';
8 | END IF;
9 | END
10 | $$;
11 |
12 | do
13 | $$
14 | DECLARE
15 | BEGIN
16 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".job_id_seq AS bigint MAXVALUE 9223372036854775807');
17 | EXCEPTION
18 | WHEN syntax_error THEN
19 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".job_id_seq MAXVALUE 9223372036854775807');
20 | END;
21 | $$;
22 |
23 | RESET search_path;
24 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Scripts/Install.v15.sql:
--------------------------------------------------------------------------------
1 | SET search_path = 'hangfire';
2 |
3 | DO
4 | $$
5 | BEGIN
6 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 15) THEN
7 | RAISE EXCEPTION 'version-already-applied';
8 | END IF;
9 | END
10 | $$;
11 |
12 | CREATE INDEX ix_hangfire_job_expireat ON "job" (expireat);
13 | CREATE INDEX ix_hangfire_list_expireat ON "list" (expireat);
14 | CREATE INDEX ix_hangfire_set_expireat ON "set" (expireat);
15 | CREATE INDEX ix_hangfire_hash_expireat ON "hash" (expireat);
16 |
17 | RESET search_path;
18 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Scripts/Install.v16.sql:
--------------------------------------------------------------------------------
1 | SET search_path = 'hangfire';
2 |
3 | DO
4 | $$
5 | BEGIN
6 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 16) THEN
7 | RAISE EXCEPTION 'version-already-applied';
8 | END IF;
9 | END
10 | $$;
11 |
12 | -- Note: job_id_seq is already bigint as per migration script v14
13 | DO
14 | $$
15 | DECLARE
16 | BEGIN
17 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".counter_id_seq AS bigint MAXVALUE 9223372036854775807');
18 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".hash_id_seq AS bigint MAXVALUE 9223372036854775807');
19 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".jobparameter_id_seq AS bigint MAXVALUE 9223372036854775807');
20 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".jobqueue_id_seq AS bigint MAXVALUE 9223372036854775807');
21 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".list_id_seq AS bigint MAXVALUE 9223372036854775807');
22 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".set_id_seq AS bigint MAXVALUE 9223372036854775807');
23 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".state_id_seq AS bigint MAXVALUE 9223372036854775807');
24 | EXCEPTION
25 | WHEN syntax_error THEN
26 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".counter_id_seq MAXVALUE 9223372036854775807');
27 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".hash_id_seq MAXVALUE 9223372036854775807');
28 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".jobparameter_id_seq MAXVALUE 9223372036854775807');
29 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".jobqueue_id_seq MAXVALUE 9223372036854775807');
30 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".list_id_seq MAXVALUE 9223372036854775807');
31 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".set_id_seq MAXVALUE 9223372036854775807');
32 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".state_id_seq MAXVALUE 9223372036854775807');
33 | END
34 | $$;
35 |
36 | RESET search_path;
37 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Scripts/Install.v17.sql:
--------------------------------------------------------------------------------
1 | SET search_path = 'hangfire';
2 |
3 | DO
4 | $$
5 | BEGIN
6 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 17) THEN
7 | RAISE EXCEPTION 'version-already-applied';
8 | END IF;
9 | END
10 | $$;
11 |
12 | CREATE INDEX IF NOT EXISTS ix_hangfire_set_key_score ON "set" (key, score);
13 |
14 | RESET search_path;
15 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Scripts/Install.v18.sql:
--------------------------------------------------------------------------------
1 | SET search_path = 'hangfire';
2 |
3 | DO
4 | $$
5 | BEGIN
6 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 18) THEN
7 | RAISE EXCEPTION 'version-already-applied';
8 | END IF;
9 | END
10 | $$;
11 |
12 | CREATE TABLE aggregatedcounter (
13 | "id" bigserial PRIMARY KEY NOT NULL,
14 | "key" text NOT NULL UNIQUE,
15 | "value" int8 NOT NULL,
16 | "expireat" timestamp
17 | );
18 |
19 | RESET search_path;
20 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Scripts/Install.v19.sql:
--------------------------------------------------------------------------------
1 | SET search_path = 'hangfire';
2 |
3 | DO $$
4 | BEGIN
5 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 19) THEN
6 | RAISE EXCEPTION 'version-already-applied';
7 | END IF;
8 | END $$;
9 |
10 | ALTER TABLE "aggregatedcounter" ALTER COLUMN "expireat" TYPE timestamp with time zone;
11 | ALTER TABLE "counter" ALTER COLUMN "expireat" TYPE timestamp with time zone;
12 | ALTER TABLE "hash" ALTER COLUMN "expireat" TYPE timestamp with time zone;
13 | ALTER TABLE "job" ALTER COLUMN "createdat" TYPE timestamp with time zone;
14 | ALTER TABLE "job" ALTER COLUMN "expireat" TYPE timestamp with time zone;
15 | ALTER TABLE "jobqueue" ALTER COLUMN "fetchedat" TYPE timestamp with time zone;
16 | ALTER TABLE "list" ALTER COLUMN "expireat" TYPE timestamp with time zone;
17 | ALTER TABLE "lock" ALTER COLUMN "acquired" TYPE timestamp with time zone;
18 | ALTER TABLE "server" ALTER COLUMN "lastheartbeat" TYPE timestamp with time zone;
19 | ALTER TABLE "set" ALTER COLUMN "expireat" TYPE timestamp with time zone;
20 | ALTER TABLE "state" ALTER COLUMN "createdat" TYPE timestamp with time zone;
21 |
22 | RESET search_path;
23 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Scripts/Install.v20.sql:
--------------------------------------------------------------------------------
1 | SET search_path = 'hangfire';
2 |
3 | DO $$
4 | BEGIN
5 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 20) THEN
6 | RAISE EXCEPTION 'version-already-applied';
7 | END IF;
8 | END $$;
9 |
10 | -- Update existing jobs, if any have empty values first
11 | UPDATE "job" SET "invocationdata" = '{}' WHERE "invocationdata" = '';
12 | UPDATE "job" SET "arguments" = '[]' WHERE "arguments" = '';
13 |
14 | -- Change the type
15 |
16 | ALTER TABLE "job" ALTER COLUMN "invocationdata" TYPE jsonb USING "invocationdata"::jsonb;
17 | ALTER TABLE "job" ALTER COLUMN "arguments" TYPE jsonb USING "arguments"::jsonb;
18 | ALTER TABLE "server" ALTER COLUMN "data" TYPE jsonb USING "data"::jsonb;
19 | ALTER TABLE "state" ALTER COLUMN "data" TYPE jsonb USING "data"::jsonb;
20 |
21 | RESET search_path;
22 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Scripts/Install.v21.sql:
--------------------------------------------------------------------------------
1 | SET search_path = 'hangfire';
2 |
3 | DO $$
4 | BEGIN
5 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 21) THEN
6 | RAISE EXCEPTION 'version-already-applied';
7 | END IF;
8 | END $$;
9 |
10 | -- Set REPLICA IDENTITY to allow replication
11 | ALTER TABLE "lock" REPLICA IDENTITY USING INDEX "lock_resource_key";
12 |
13 | RESET search_path;
14 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Scripts/Install.v22.sql:
--------------------------------------------------------------------------------
1 | SET search_path = 'hangfire';
2 |
3 | DO $$
4 | BEGIN
5 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 22) THEN
6 | RAISE EXCEPTION 'version-already-applied';
7 | END IF;
8 | END $$;
9 |
10 | DROP INDEX IF EXISTS jobqueue_queue_fetchat_jobId;
11 | CREATE INDEX IF NOT EXISTS ix_hangfire_jobqueue_fetchedat_queue_jobid ON jobqueue USING btree (fetchedat nulls first, queue, jobid);
12 |
13 | RESET search_path;
14 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Scripts/Install.v23.sql:
--------------------------------------------------------------------------------
1 | SET search_path = 'hangfire';
2 |
3 | DO $$
4 | BEGIN
5 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 23) THEN
6 | RAISE EXCEPTION 'version-already-applied';
7 | END IF;
8 | END $$;
9 |
10 | DROP INDEX IF EXISTS ix_hangfire_job_statename_is_not_null;
11 |
12 | DO $$
13 | BEGIN
14 | IF current_setting('server_version_num')::int >= 110000 THEN
15 | EXECUTE 'CREATE INDEX ix_hangfire_job_statename_is_not_null ON job USING btree(statename) INCLUDE (id) WHERE statename IS NOT NULL';
16 | ELSE
17 | CREATE INDEX ix_hangfire_job_statename_is_not_null ON job USING btree(statename) WHERE statename IS NOT NULL;
18 | END IF;
19 | END $$;
20 |
21 | RESET search_path;
22 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Scripts/Install.v3.sql:
--------------------------------------------------------------------------------
1 | DO
2 | $$
3 | BEGIN
4 | IF NOT EXISTS(
5 | SELECT schema_name
6 | FROM information_schema.schemata
7 | WHERE schema_name = 'hangfire'
8 | )
9 | THEN
10 | EXECUTE 'CREATE SCHEMA "hangfire";';
11 | END IF;
12 |
13 | END
14 | $$;
15 |
16 | SET search_path = 'hangfire';
17 | --
18 | -- Table structure for table `Schema`
19 | --
20 |
21 | CREATE TABLE IF NOT EXISTS "schema"
22 | (
23 | "version" INT NOT NULL,
24 | PRIMARY KEY ("version")
25 | );
26 |
27 |
28 | DO
29 | $$
30 | BEGIN
31 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 3) THEN
32 | RAISE EXCEPTION 'version-already-applied';
33 | END IF;
34 | END
35 | $$;
36 |
37 | INSERT INTO "schema"("version")
38 | VALUES ('1');
39 |
40 | --
41 | -- Table structure for table `Counter`
42 | --
43 |
44 | CREATE TABLE IF NOT EXISTS "counter"
45 | (
46 | "id" SERIAL NOT NULL,
47 | "key" VARCHAR(100) NOT NULL,
48 | "value" SMALLINT NOT NULL,
49 | "expireat" TIMESTAMP NULL,
50 | PRIMARY KEY ("id")
51 | );
52 |
53 | DO
54 | $$
55 | BEGIN
56 | BEGIN
57 | CREATE INDEX "ix_hangfire_counter_key" ON "counter" ("key");
58 | EXCEPTION
59 | WHEN duplicate_table THEN RAISE NOTICE 'INDEX ix_hangfire_counter_key already exists.';
60 | END;
61 | END;
62 | $$;
63 |
64 | --
65 | -- Table structure for table `Hash`
66 | --
67 |
68 | CREATE TABLE IF NOT EXISTS "hash"
69 | (
70 | "id" SERIAL NOT NULL,
71 | "key" VARCHAR(100) NOT NULL,
72 | "field" VARCHAR(100) NOT NULL,
73 | "value" TEXT NULL,
74 | "expireat" TIMESTAMP NULL,
75 | PRIMARY KEY ("id"),
76 | UNIQUE ("key", "field")
77 | );
78 |
79 |
80 | --
81 | -- Table structure for table `Job`
82 | --
83 |
84 | CREATE TABLE IF NOT EXISTS "job"
85 | (
86 | "id" SERIAL NOT NULL,
87 | "stateid" INT NULL,
88 | "statename" VARCHAR(20) NULL,
89 | "invocationdata" TEXT NOT NULL,
90 | "arguments" TEXT NOT NULL,
91 | "createdat" TIMESTAMP NOT NULL,
92 | "expireat" TIMESTAMP NULL,
93 | PRIMARY KEY ("id")
94 | );
95 |
96 | DO
97 | $$
98 | BEGIN
99 | BEGIN
100 | CREATE INDEX "ix_hangfire_job_statename" ON "job" ("statename");
101 | EXCEPTION
102 | WHEN duplicate_table THEN RAISE NOTICE 'INDEX "ix_hangfire_job_statename" already exists.';
103 | END;
104 | END;
105 | $$;
106 |
107 | --
108 | -- Table structure for table `State`
109 | --
110 |
111 | CREATE TABLE IF NOT EXISTS "state"
112 | (
113 | "id" SERIAL NOT NULL,
114 | "jobid" INT NOT NULL,
115 | "name" VARCHAR(20) NOT NULL,
116 | "reason" VARCHAR(100) NULL,
117 | "createdat" TIMESTAMP NOT NULL,
118 | "data" TEXT NULL,
119 | PRIMARY KEY ("id"),
120 | FOREIGN KEY ("jobid") REFERENCES "job" ("id") ON UPDATE CASCADE ON DELETE CASCADE
121 | );
122 |
123 | DO
124 | $$
125 | BEGIN
126 | BEGIN
127 | CREATE INDEX "ix_hangfire_state_jobid" ON "state" ("jobid");
128 | EXCEPTION
129 | WHEN duplicate_table THEN RAISE NOTICE 'INDEX "ix_hangfire_state_jobid" already exists.';
130 | END;
131 | END;
132 | $$;
133 |
134 |
135 |
136 | --
137 | -- Table structure for table `JobQueue`
138 | --
139 |
140 | CREATE TABLE IF NOT EXISTS "jobqueue"
141 | (
142 | "id" SERIAL NOT NULL,
143 | "jobid" INT NOT NULL,
144 | "queue" VARCHAR(20) NOT NULL,
145 | "fetchedat" TIMESTAMP NULL,
146 | PRIMARY KEY ("id")
147 | );
148 |
149 | DO
150 | $$
151 | BEGIN
152 | BEGIN
153 | CREATE INDEX "ix_hangfire_jobqueue_queueandfetchedat" ON "jobqueue" ("queue", "fetchedat");
154 | EXCEPTION
155 | WHEN duplicate_table THEN RAISE NOTICE 'INDEX "ix_hangfire_jobqueue_queueandfetchedat" already exists.';
156 | END;
157 | END;
158 | $$;
159 |
160 |
161 | --
162 | -- Table structure for table `List`
163 | --
164 |
165 | CREATE TABLE IF NOT EXISTS "list"
166 | (
167 | "id" SERIAL NOT NULL,
168 | "key" VARCHAR(100) NOT NULL,
169 | "value" TEXT NULL,
170 | "expireat" TIMESTAMP NULL,
171 | PRIMARY KEY ("id")
172 | );
173 |
174 |
175 | --
176 | -- Table structure for table `Server`
177 | --
178 |
179 | CREATE TABLE IF NOT EXISTS "server"
180 | (
181 | "id" VARCHAR(50) NOT NULL,
182 | "data" TEXT NULL,
183 | "lastheartbeat" TIMESTAMP NOT NULL,
184 | PRIMARY KEY ("id")
185 | );
186 |
187 |
188 | --
189 | -- Table structure for table `Set`
190 | --
191 |
192 | CREATE TABLE IF NOT EXISTS "set"
193 | (
194 | "id" SERIAL NOT NULL,
195 | "key" VARCHAR(100) NOT NULL,
196 | "score" FLOAT8 NOT NULL,
197 | "value" TEXT NOT NULL,
198 | "expireat" TIMESTAMP NULL,
199 | PRIMARY KEY ("id"),
200 | UNIQUE ("key", "value")
201 | );
202 |
203 |
204 | --
205 | -- Table structure for table `JobParameter`
206 | --
207 |
208 | CREATE TABLE IF NOT EXISTS "jobparameter"
209 | (
210 | "id" SERIAL NOT NULL,
211 | "jobid" INT NOT NULL,
212 | "name" VARCHAR(40) NOT NULL,
213 | "value" TEXT NULL,
214 | PRIMARY KEY ("id"),
215 | FOREIGN KEY ("jobid") REFERENCES "job" ("id") ON UPDATE CASCADE ON DELETE CASCADE
216 | );
217 |
218 | DO
219 | $$
220 | BEGIN
221 | BEGIN
222 | CREATE INDEX "ix_hangfire_jobparameter_jobidandname" ON "jobparameter" ("jobid", "name");
223 | EXCEPTION
224 | WHEN duplicate_table THEN RAISE NOTICE 'INDEX "ix_hangfire_jobparameter_jobidandname" already exists.';
225 | END;
226 | END;
227 | $$;
228 |
229 | CREATE TABLE IF NOT EXISTS "lock"
230 | (
231 | "resource" VARCHAR(100) NOT NULL,
232 | UNIQUE ("resource")
233 | );
234 |
235 | RESET search_path;
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Scripts/Install.v4.sql:
--------------------------------------------------------------------------------
1 | SET search_path = 'hangfire';
2 | --
3 | -- Table structure for table `Schema`
4 | --
5 |
6 | DO
7 | $$
8 | BEGIN
9 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 4) THEN
10 | RAISE EXCEPTION 'version-already-applied';
11 | END IF;
12 | END
13 | $$;
14 |
15 | ALTER TABLE "counter"
16 | ADD COLUMN "updatecount" integer NOT NULL DEFAULT 0;
17 | ALTER TABLE "lock"
18 | ADD COLUMN "updatecount" integer NOT NULL DEFAULT 0;
19 | ALTER TABLE "hash"
20 | ADD COLUMN "updatecount" integer NOT NULL DEFAULT 0;
21 | ALTER TABLE "job"
22 | ADD COLUMN "updatecount" integer NOT NULL DEFAULT 0;
23 | ALTER TABLE "jobparameter"
24 | ADD COLUMN "updatecount" integer NOT NULL DEFAULT 0;
25 | ALTER TABLE "jobqueue"
26 | ADD COLUMN "updatecount" integer NOT NULL DEFAULT 0;
27 | ALTER TABLE "list"
28 | ADD COLUMN "updatecount" integer NOT NULL DEFAULT 0;
29 | ALTER TABLE "server"
30 | ADD COLUMN "updatecount" integer NOT NULL DEFAULT 0;
31 | ALTER TABLE "set"
32 | ADD COLUMN "updatecount" integer NOT NULL DEFAULT 0;
33 | ALTER TABLE "state"
34 | ADD COLUMN "updatecount" integer NOT NULL DEFAULT 0;
35 |
36 | RESET search_path;
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Scripts/Install.v5.sql:
--------------------------------------------------------------------------------
1 | SET search_path = 'hangfire';
2 | --
3 | -- Table structure for table `Schema`
4 | --
5 |
6 | DO
7 | $$
8 | BEGIN
9 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 5) THEN
10 | RAISE EXCEPTION 'version-already-applied';
11 | END IF;
12 | END
13 | $$;
14 |
15 | ALTER TABLE "server"
16 | ALTER COLUMN "id" TYPE VARCHAR(100);
17 |
18 | RESET search_path;
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Scripts/Install.v6.sql:
--------------------------------------------------------------------------------
1 | SET search_path = 'hangfire';
2 | --
3 | -- Adds indices, greatly speeds-up deleting old jobs.
4 | --
5 |
6 | DO
7 | $$
8 | BEGIN
9 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 6) THEN
10 | RAISE EXCEPTION 'version-already-applied';
11 | END IF;
12 | END
13 | $$;
14 |
15 |
16 | DO
17 | $$
18 | BEGIN
19 | BEGIN
20 | CREATE INDEX "ix_hangfire_counter_expireat" ON "counter" ("expireat");
21 | EXCEPTION
22 | WHEN duplicate_table THEN RAISE NOTICE 'INDEX ix_hangfire_counter_expireat already exists.';
23 | END;
24 | END;
25 | $$;
26 |
27 | DO
28 | $$
29 | BEGIN
30 | BEGIN
31 | CREATE INDEX "ix_hangfire_jobqueue_jobidandqueue" ON "jobqueue" ("jobid", "queue");
32 | EXCEPTION
33 | WHEN duplicate_table THEN RAISE NOTICE 'INDEX "ix_hangfire_jobqueue_jobidandqueue" already exists.';
34 | END;
35 | END;
36 | $$;
37 |
38 | RESET search_path;
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Scripts/Install.v7.sql:
--------------------------------------------------------------------------------
1 | SET search_path = 'hangfire';
2 | --
3 | -- Table structure for table `Schema`
4 | --
5 |
6 | DO
7 | $$
8 | BEGIN
9 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 7) THEN
10 | RAISE EXCEPTION 'version-already-applied';
11 | END IF;
12 | END
13 | $$;
14 |
15 | ALTER TABLE "lock"
16 | ADD COLUMN acquired timestamp without time zone;
17 |
18 | RESET search_path;
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Scripts/Install.v8.sql:
--------------------------------------------------------------------------------
1 | SET search_path = 'hangfire';
2 | --
3 | -- Table structure for table `Schema`
4 | --
5 |
6 | DO
7 | $$
8 | BEGIN
9 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 8) THEN
10 | RAISE EXCEPTION 'version-already-applied';
11 | END IF;
12 | END
13 | $$;
14 |
15 | ALTER TABLE "counter"
16 | ALTER COLUMN value TYPE bigint;
17 | ALTER TABLE "counter"
18 | DROP COLUMN updatecount RESTRICT;
19 |
20 | RESET search_path;
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Scripts/Install.v9.sql:
--------------------------------------------------------------------------------
1 | SET search_path = 'hangfire';
2 | --
3 | -- Table structure for table `Schema`
4 | --
5 |
6 | DO
7 | $$
8 | BEGIN
9 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 9) THEN
10 | RAISE EXCEPTION 'version-already-applied';
11 | END IF;
12 | END
13 | $$;
14 |
15 | ALTER TABLE "lock"
16 | ALTER COLUMN "resource" TYPE TEXT;
17 |
18 | RESET search_path;
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Utils/AutoResetEventRegistry.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Concurrent;
2 | using System.Collections.Generic;
3 | using System.Threading;
4 |
5 | namespace Hangfire.PostgreSql.Utils
6 | {
7 | ///
8 | /// Represents a registry for managing AutoResetEvent instances using event keys.
9 | ///
10 | public class AutoResetEventRegistry
11 | {
12 | private readonly ConcurrentDictionary _events = new();
13 |
14 | ///
15 | /// Retrieves the wait handles associated with the specified event keys.
16 | ///
17 | /// The event keys.
18 | /// An enumerable of wait handles.
19 | public IEnumerable GetWaitHandles(IEnumerable eventKeys)
20 | {
21 | foreach (string eventKey in eventKeys)
22 | {
23 | AutoResetEvent newHandle = _events.GetOrAdd(eventKey, _ => new AutoResetEvent(false));
24 | yield return newHandle;
25 | }
26 | }
27 |
28 | ///
29 | /// Sets the specified event.
30 | ///
31 | /// The event key.
32 | public void Set(string eventKey)
33 | {
34 | if (_events.TryGetValue(eventKey, out AutoResetEvent handle))
35 | {
36 | handle.Set();
37 | }
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Utils/DbConnectionExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Data;
2 | using Npgsql;
3 |
4 | namespace Hangfire.PostgreSql.Utils;
5 |
6 | internal static class DbConnectionExtensions
7 | {
8 | private static bool? _supportsNotifications;
9 |
10 | internal static bool SupportsNotifications(this IDbConnection connection)
11 | {
12 | if (_supportsNotifications.HasValue)
13 | {
14 | return _supportsNotifications.Value;
15 | }
16 |
17 | if (connection is not NpgsqlConnection npgsqlConnection)
18 | {
19 | _supportsNotifications = false;
20 | return false;
21 | }
22 |
23 | if (npgsqlConnection.State != ConnectionState.Open)
24 | {
25 | npgsqlConnection.Open();
26 | }
27 |
28 | _supportsNotifications = npgsqlConnection.PostgreSqlVersion.Major >= 11;
29 | return _supportsNotifications.Value;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Utils/ExceptionTypeHelper.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire. Copyright © 2022 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | // Borrowed from Hangfire
17 |
18 | using System;
19 |
20 | namespace Hangfire.PostgreSql.Utils
21 | {
22 | internal static class ExceptionTypeHelper
23 | {
24 | #if !NETSTANDARD1_3
25 | private static readonly Type StackOverflowType = typeof(StackOverflowException);
26 | #endif
27 | private static readonly Type OutOfMemoryType = typeof(OutOfMemoryException);
28 |
29 | internal static bool IsCatchableExceptionType(this Exception e)
30 | {
31 | var type = e.GetType();
32 | return
33 | #if !NETSTANDARD1_3
34 | type != StackOverflowType &&
35 | #endif
36 | type != OutOfMemoryType;
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Utils/TimestampHelper.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire. Copyright © 2022 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | // Borrowed from Hangfire
17 |
18 | using System;
19 |
20 | namespace Hangfire.PostgreSql.Utils
21 | {
22 | internal static class TimestampHelper
23 | {
24 | public static long GetTimestamp()
25 | {
26 | #if NETCOREAPP3_0
27 | return Environment.TickCount64;
28 | #else
29 | return Environment.TickCount;
30 | #endif
31 | }
32 |
33 | public static TimeSpan Elapsed(long timestamp)
34 | {
35 | long now = GetTimestamp();
36 | return Elapsed(now, timestamp);
37 | }
38 |
39 | public static TimeSpan Elapsed(long now, long timestamp)
40 | {
41 | #if NETCOREAPP3_0
42 | return TimeSpan.FromMilliseconds(now - timestamp);
43 | #else
44 | return TimeSpan.FromMilliseconds(unchecked((int)now - (int)timestamp));
45 | #endif
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Utils/TransactionHelpers.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Transactions;
3 |
4 | namespace Hangfire.PostgreSql.Utils
5 | {
6 | public static class TransactionHelpers
7 | {
8 | internal static TransactionScope CreateTransactionScope(IsolationLevel? isolationLevel = IsolationLevel.ReadCommitted, bool enlist = true, TimeSpan? timeout = null)
9 | {
10 | TransactionScopeOption scopeOption = TransactionScopeOption.RequiresNew;
11 | if (enlist)
12 | {
13 | Transaction currentTransaction = Transaction.Current;
14 | if (currentTransaction != null)
15 | {
16 | isolationLevel = currentTransaction.IsolationLevel;
17 | scopeOption = TransactionScopeOption.Required;
18 | }
19 | }
20 |
21 | return new TransactionScope(
22 | scopeOption,
23 | new TransactionOptions {
24 | IsolationLevel = isolationLevel.GetValueOrDefault(IsolationLevel.ReadCommitted),
25 | Timeout = timeout.GetValueOrDefault(TransactionManager.DefaultTimeout),
26 | });
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Hangfire.PostgreSql/Utils/TryExecute.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.PostgreSql.
2 | // Copyright © 2014 Frank Hommers .
3 | //
4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.PostgreSql. If not, see .
16 | //
17 | // This work is based on the work of Sergey Odinokov, author of
18 | // Hangfire.
19 | //
20 | // Special thanks goes to him.
21 |
22 | using System;
23 | // ReSharper disable ArrangeDefaultValueWhenTypeNotEvident
24 |
25 | namespace Hangfire.PostgreSql.Utils
26 | {
27 | public static class Utils
28 | {
29 | public static bool TryExecute(
30 | Func func,
31 | out T result,
32 | Func swallowException = default,
33 | int? tryCount = default)
34 | {
35 | while (tryCount == default || tryCount-- > 0)
36 | {
37 | try
38 | {
39 | result = func();
40 | return true;
41 | }
42 | catch (Exception ex)
43 | {
44 | if (swallowException != null && !swallowException(ex))
45 | {
46 | throw;
47 | }
48 | }
49 | }
50 | result = default;
51 | return false;
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/tests/Hangfire.PostgreSql.Tests/AssemblyAttributes.cs:
--------------------------------------------------------------------------------
1 | using Xunit;
2 |
3 | // Running tests in parallel actually takes more time when we are cleaning the database for majority of tests. It's quicker to just let them run sequentially.
4 | [assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly, DisableTestParallelization = true)]
--------------------------------------------------------------------------------
/tests/Hangfire.PostgreSql.Tests/CountersAggregatorFacts.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using Dapper;
4 | using Hangfire.PostgreSql.Tests.Utils;
5 | using Npgsql;
6 | using Xunit;
7 |
8 | namespace Hangfire.PostgreSql.Tests;
9 |
10 | public class CountersAggregatorFacts : IClassFixture
11 | {
12 | private static readonly string _schemaName = ConnectionUtils.GetSchemaName();
13 |
14 | private readonly CancellationToken _token;
15 | private readonly PostgreSqlStorageFixture _fixture;
16 |
17 | public CountersAggregatorFacts(PostgreSqlStorageFixture fixture)
18 | {
19 | CancellationTokenSource cts = new();
20 | _token = cts.Token;
21 | _fixture = fixture;
22 | _fixture.SetupOptions(o => o.CountersAggregateInterval = TimeSpan.FromMinutes(5));
23 | }
24 |
25 | [Fact]
26 | [CleanDatabase]
27 | public void Execute_AggregatesCounters()
28 | {
29 | UseConnection((connection, manager) => {
30 | CreateEntry(1);
31 | CreateEntry(5);
32 | CreateEntry(15);
33 | CreateEntry(5, "key2");
34 | CreateEntry(10, "key2");
35 |
36 | manager.Execute(_token);
37 |
38 | Assert.Equal(21, GetAggregatedCounters(connection));
39 | Assert.Equal(15, GetAggregatedCounters(connection, "key2"));
40 | Assert.Null(GetRegularCounters(connection));
41 | Assert.Null(GetRegularCounters(connection, "key2"));
42 | return;
43 |
44 | void CreateEntry(long value, string key = "key")
45 | {
46 | CreateCounterEntry(connection, value, key);
47 | }
48 | });
49 | }
50 |
51 | private void UseConnection(Action action)
52 | {
53 | PostgreSqlStorage storage = _fixture.SafeInit();
54 | CountersAggregator aggregator = new(storage, TimeSpan.Zero);
55 | action(storage.CreateAndOpenConnection(), aggregator);
56 | }
57 |
58 | private static void CreateCounterEntry(NpgsqlConnection connection, long? value, string key = "key")
59 | {
60 | value ??= 1;
61 | string insertSql =
62 | $"""
63 | INSERT INTO "{_schemaName}"."counter"("key", "value", "expireat")
64 | VALUES (@Key, @Value, null)
65 | """;
66 |
67 | connection.Execute(insertSql, new { Key = key, Value = value });
68 | }
69 |
70 | private static long GetAggregatedCounters(NpgsqlConnection connection, string key = "key")
71 | {
72 | return connection.QuerySingle(
73 | $"""
74 | SELECT "value"
75 | FROM {_schemaName}."aggregatedcounter"
76 | WHERE "key" = @Key
77 | """, new { Key = key });
78 | }
79 |
80 | private static long? GetRegularCounters(NpgsqlConnection connection, string key = "key")
81 | {
82 | return connection.QuerySingle(
83 | $"""
84 | SELECT SUM("value")
85 | FROM {_schemaName}."counter"
86 | WHERE "key" = @Key
87 | """, new { Key = key });
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/tests/Hangfire.PostgreSql.Tests/Entities/TestJob.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Hangfire.PostgreSql.Tests.Entities
4 | {
5 | public record TestJob(long Id, string InvocationData, string Arguments, DateTime? ExpireAt, string StateName, long? StateId, DateTime CreatedAt);
6 |
7 | public class TestJobs
8 | {
9 | public void Run(string logMessage)
10 | {
11 | Console.WriteLine("Running test job: {0}", logMessage);
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/tests/Hangfire.PostgreSql.Tests/ExpirationManagerFacts.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Globalization;
4 | using System.Linq;
5 | using System.Threading;
6 | using Dapper;
7 | using Hangfire.PostgreSql.Tests.Utils;
8 | using Npgsql;
9 | using Xunit;
10 |
11 | namespace Hangfire.PostgreSql.Tests
12 | {
13 | public class ExpirationManagerFacts : IClassFixture
14 | {
15 | private readonly PostgreSqlStorageFixture _fixture;
16 | private readonly CancellationToken _token;
17 |
18 | public ExpirationManagerFacts(PostgreSqlStorageFixture fixture)
19 | {
20 | CancellationTokenSource cts = new();
21 | _token = cts.Token;
22 | _fixture = fixture;
23 | _fixture.SetupOptions(o => o.DeleteExpiredBatchSize = 2);
24 | }
25 |
26 | [Fact]
27 | public void Ctor_ThrowsAnException_WhenStorageIsNull()
28 | {
29 | Assert.Throws(() => new ExpirationManager(null));
30 | }
31 |
32 | [Fact]
33 | [CleanDatabase]
34 | public void Execute_RemovesOutdatedRecords()
35 | {
36 | UseConnection((connection, manager) => {
37 | long CreateEntry(string key)
38 | {
39 | return CreateExpirationEntry(connection, DateTime.UtcNow.AddMonths(-1), key);
40 | }
41 |
42 | List entryIds = Enumerable.Range(1, 3).Select(i => CreateEntry($"key{i}")).ToList();
43 |
44 | manager.Execute(_token);
45 |
46 | entryIds.ForEach(entryId => Assert.True(IsEntryExpired(connection, entryId)));
47 | });
48 | }
49 |
50 | [Fact]
51 | [CleanDatabase]
52 | public void Execute_DoesNotRemoveEntries_WithNoExpirationTimeSet()
53 | {
54 | UseConnection((connection, manager) => {
55 | long entryId = CreateExpirationEntry(connection, null);
56 |
57 | manager.Execute(_token);
58 |
59 | Assert.False(IsEntryExpired(connection, entryId));
60 | });
61 | }
62 |
63 | [Fact]
64 | [CleanDatabase]
65 | public void Execute_DoesNotRemoveEntries_WithFreshExpirationTime()
66 | {
67 | UseConnection((connection, manager) => {
68 | long entryId = CreateExpirationEntry(connection, DateTime.Now.AddMonths(1));
69 |
70 | manager.Execute(_token);
71 |
72 | Assert.False(IsEntryExpired(connection, entryId));
73 | });
74 | }
75 |
76 | [Fact]
77 | [CleanDatabase]
78 | public void Execute_Processes_CounterTable()
79 | {
80 | UseConnection((connection, manager) => {
81 | // Arrange
82 | string createSql = $@"
83 | INSERT INTO ""{GetSchemaName()}"".""counter"" (""key"", ""value"", ""expireat"")
84 | VALUES ('key', 1, @ExpireAt)
85 | ";
86 | connection.Execute(createSql, new { ExpireAt = DateTime.UtcNow.AddMonths(-1) });
87 |
88 | // Act
89 | manager.Execute(_token);
90 |
91 | // Assert
92 | Assert.Equal(0, connection.QuerySingle($@"SELECT COUNT(*) FROM ""{GetSchemaName()}"".""counter"""));
93 | });
94 | }
95 |
96 | [Fact]
97 | [CleanDatabase]
98 | public void Execute_Aggregates_CounterTable()
99 | {
100 | UseConnection((connection, manager) => {
101 | // Arrange
102 | string createSql = $@"
103 | INSERT INTO ""{GetSchemaName()}"".""counter"" (""key"", ""value"")
104 | VALUES ('stats:succeeded', 1)
105 | ";
106 | for (int i = 0; i < 5; i++)
107 | {
108 | connection.Execute(createSql);
109 | }
110 |
111 | // Act
112 | manager.Execute(_token);
113 |
114 | // Assert
115 | Assert.Equal(1, connection.QuerySingle($@"SELECT COUNT(*) FROM ""{GetSchemaName()}"".""counter"""));
116 | Assert.Equal(5, connection.QuerySingle($@"SELECT SUM(""value"") FROM ""{GetSchemaName()}"".""counter"""));
117 | });
118 | }
119 |
120 | [Fact]
121 | [CleanDatabase]
122 | public void Execute_Processes_JobTable()
123 | {
124 | UseConnection((connection, manager) => {
125 | // Arrange
126 | string createSql = $@"
127 | INSERT INTO ""{GetSchemaName()}"".""job"" (""invocationdata"", ""arguments"", ""createdat"", ""expireat"")
128 | VALUES ('{{}}', '[]', NOW(), @ExpireAt)
129 | ";
130 | connection.Execute(createSql, new { ExpireAt = DateTime.UtcNow.AddMonths(-1) });
131 |
132 | // Act
133 | manager.Execute(_token);
134 |
135 | // Assert
136 | Assert.Equal(0, connection.QuerySingle($@"SELECT COUNT(*) FROM ""{GetSchemaName()}"".""job"""));
137 | });
138 | }
139 |
140 | [Fact]
141 | [CleanDatabase]
142 | public void Execute_Processes_ListTable()
143 | {
144 | UseConnection((connection, manager) => {
145 | // Arrange
146 | string createSql = $@"INSERT INTO ""{GetSchemaName()}"".""list"" (""key"", ""expireat"") VALUES ('key', @ExpireAt)";
147 | connection.Execute(createSql, new { ExpireAt = DateTime.UtcNow.AddMonths(-1) });
148 |
149 | // Act
150 | manager.Execute(_token);
151 |
152 | // Assert
153 | Assert.Equal(0, connection.QuerySingle($@"SELECT COUNT(*) FROM ""{GetSchemaName()}"".""list"""));
154 | });
155 | }
156 |
157 | [Fact]
158 | [CleanDatabase]
159 | public void Execute_Processes_SetTable()
160 | {
161 | UseConnection((connection, manager) => {
162 | // Arrange
163 | string createSql = $@"INSERT INTO ""{GetSchemaName()}"".""set"" (""key"", ""score"", ""value"", ""expireat"") VALUES ('key', 0, '', @ExpireAt)";
164 | connection.Execute(createSql, new { ExpireAt = DateTime.UtcNow.AddMonths(-1) });
165 |
166 | // Act
167 | manager.Execute(_token);
168 |
169 | // Assert
170 | Assert.Equal(0, connection.Query($@"SELECT COUNT(*) FROM ""{GetSchemaName()}"".""set""").Single());
171 | });
172 | }
173 |
174 | [Fact]
175 | [CleanDatabase]
176 | public void Execute_Processes_HashTable()
177 | {
178 | UseConnection((connection, manager) => {
179 | // Arrange
180 | string createSql = $@"
181 | INSERT INTO ""{GetSchemaName()}"".""hash"" (""key"", ""field"", ""value"", ""expireat"")
182 | VALUES ('key', 'field', '', @ExpireAt)";
183 | connection.Execute(createSql, new { ExpireAt = DateTime.UtcNow.AddMonths(-1) });
184 |
185 | // Act
186 | manager.Execute(_token);
187 |
188 | // Assert
189 | Assert.Equal(0, connection.QuerySingle($@"SELECT COUNT(*) FROM ""{GetSchemaName()}"".""hash"""));
190 | });
191 | }
192 |
193 | private static long CreateExpirationEntry(NpgsqlConnection connection, DateTime? expireAt, string key = "key")
194 | {
195 | string insertSqlNull = $@"
196 | INSERT INTO ""{GetSchemaName()}"".""counter""(""key"", ""value"", ""expireat"")
197 | VALUES (@Key, 1, null) RETURNING ""id""
198 | ";
199 |
200 | string insertSqlValue = $@"
201 | INSERT INTO ""{GetSchemaName()}"".""counter""(""key"", ""value"", ""expireat"")
202 | VALUES (@Key, 1, NOW() - interval '{{0}} seconds') RETURNING ""id""
203 | ";
204 |
205 | string insertSql = expireAt == null
206 | ? insertSqlNull
207 | : string.Format(CultureInfo.InvariantCulture, insertSqlValue,
208 | ((long)(DateTime.UtcNow - expireAt.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture));
209 |
210 | return connection.QuerySingle(insertSql, new { Key = key });
211 | }
212 |
213 | private static bool IsEntryExpired(NpgsqlConnection connection, long entryId)
214 | {
215 | return connection.QuerySingle($@"SELECT COUNT(*) FROM ""{GetSchemaName()}"".""counter"" WHERE ""id"" = @Id", new { Id = entryId }) == 0;
216 | }
217 |
218 | private void UseConnection(Action action)
219 | {
220 | PostgreSqlStorage storage = _fixture.SafeInit();
221 | ExpirationManager manager = new ExpirationManager(storage, TimeSpan.Zero);
222 | action(storage.CreateAndOpenConnection(), manager);
223 | }
224 |
225 | private static string GetSchemaName()
226 | {
227 | return ConnectionUtils.GetSchemaName();
228 | }
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/tests/Hangfire.PostgreSql.Tests/FirstClassQueueFeatureSupportTests.cs:
--------------------------------------------------------------------------------
1 | using System.Threading;
2 | using Hangfire.PostgreSql.Tests.Entities;
3 | using Hangfire.PostgreSql.Tests.Utils;
4 | using Hangfire.Storage;
5 | using Hangfire.Storage.Monitoring;
6 | using Xunit;
7 |
8 | namespace Hangfire.PostgreSql.Tests;
9 |
10 | public class FirstClassQueueFeatureSupportTests
11 | {
12 | public FirstClassQueueFeatureSupportTests()
13 | {
14 | JobStorage.Current = new PostgreSqlStorage(ConnectionUtils.GetDefaultConnectionFactory());
15 | }
16 |
17 | [Fact]
18 | public void HasFlag_ShouldReturnTrue_ForJobQueueProperty()
19 | {
20 | bool supportJobQueueProperty = JobStorage.Current.HasFeature(JobStorageFeatures.JobQueueProperty);
21 | Assert.True(supportJobQueueProperty);
22 | }
23 |
24 | [Fact]
25 | [CleanDatabase]
26 | public void EnqueueJobWithSpecificQueue_ShouldEnqueueCorrectlyAndJobMustBeProcessedInThatQueue()
27 | {
28 | BackgroundJob.Enqueue("critical", job => job.Run("critical"));
29 | BackgroundJob.Enqueue("offline", job => job.Run("offline"));
30 |
31 | BackgroundJobServer unused = new(new BackgroundJobServerOptions() {
32 | Queues = new[] { "critical" },
33 | });
34 |
35 | Thread.Sleep(200);
36 |
37 | IMonitoringApi monitoringApi = JobStorage.Current.GetMonitoringApi();
38 |
39 | JobList jobsInCriticalQueue = monitoringApi.EnqueuedJobs("critical", 0, 10);
40 | JobList jobsInOfflineQueue = monitoringApi.EnqueuedJobs("offline", 0, 10);
41 |
42 | Assert.Empty(jobsInCriticalQueue); //Job from 'critical' queue must be processed by the server
43 | Assert.NotEmpty(jobsInOfflineQueue); //Job from 'offline' queue must be left untouched because no server is processing it
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/tests/Hangfire.PostgreSql.Tests/GlobalSuppressions.cs:
--------------------------------------------------------------------------------
1 | // This file is used by Code Analysis to maintain SuppressMessage
2 | // attributes that are applied to this project.
3 | // Project-level suppressions either have no target or are given
4 | // a specific target and scoped to a namespace, type, member, etc.
5 |
6 | using System.Diagnostics.CodeAnalysis;
7 |
8 | [assembly: SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "This is a test project, underscores in test names are allowed", Scope = "module")]
9 |
--------------------------------------------------------------------------------
/tests/Hangfire.PostgreSql.Tests/Hangfire.PostgreSql.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Hangfire.PostgreSql.Tests
5 | net9.0
6 | Hangfire.PostgreSql.Tests
7 | Hangfire.PostgreSql.Tests
8 | true
9 | default
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | all
24 | runtime; build; native; contentfiles; analyzers; buildtransitive
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/tests/Hangfire.PostgreSql.Tests/PersistentJobQueueProviderCollectionFacts.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using Moq;
4 | using Xunit;
5 |
6 | namespace Hangfire.PostgreSql.Tests
7 | {
8 | public class PersistentJobQueueProviderCollectionFacts
9 | {
10 | private static readonly string[] _queues = { "default", "critical" };
11 | private readonly Mock _defaultProvider;
12 | private readonly Mock _provider;
13 |
14 | public PersistentJobQueueProviderCollectionFacts()
15 | {
16 | _defaultProvider = new Mock();
17 | _provider = new Mock();
18 | }
19 |
20 | [Fact]
21 | public void Ctor_ThrowsAnException_WhenDefaultProviderIsNull()
22 | {
23 | Assert.Throws(() => new PersistentJobQueueProviderCollection(null));
24 | }
25 |
26 | [Fact]
27 | public void Enumeration_IncludesTheDefaultProvider()
28 | {
29 | PersistentJobQueueProviderCollection collection = CreateCollection();
30 |
31 | IPersistentJobQueueProvider[] result = collection.ToArray();
32 |
33 | Assert.Single(result);
34 | Assert.Same(_defaultProvider.Object, result[0]);
35 | }
36 |
37 | [Fact]
38 | public void GetProvider_ReturnsTheDefaultProvider_WhenProviderCanNotBeResolvedByQueue()
39 | {
40 | PersistentJobQueueProviderCollection collection = CreateCollection();
41 |
42 | IPersistentJobQueueProvider provider = collection.GetProvider("queue");
43 |
44 | Assert.Same(_defaultProvider.Object, provider);
45 | }
46 |
47 | [Fact]
48 | public void Add_ThrowsAnException_WhenProviderIsNull()
49 | {
50 | PersistentJobQueueProviderCollection collection = CreateCollection();
51 |
52 | ArgumentNullException exception = Assert.Throws(() => collection.Add(null, _queues));
53 |
54 | Assert.Equal("provider", exception.ParamName);
55 | }
56 |
57 | [Fact]
58 | public void Add_ThrowsAnException_WhenQueuesCollectionIsNull()
59 | {
60 | PersistentJobQueueProviderCollection collection = CreateCollection();
61 |
62 | ArgumentNullException exception = Assert.Throws(() => collection.Add(_provider.Object, null));
63 |
64 | Assert.Equal("queues", exception.ParamName);
65 | }
66 |
67 | [Fact]
68 | public void Enumeration_ContainsAddedProvider()
69 | {
70 | PersistentJobQueueProviderCollection collection = CreateCollection();
71 |
72 | collection.Add(_provider.Object, _queues);
73 |
74 | Assert.Contains(_provider.Object, collection);
75 | }
76 |
77 | [Fact]
78 | public void GetProvider_CanBeResolved_ByAnyQueue()
79 | {
80 | PersistentJobQueueProviderCollection collection = CreateCollection();
81 | collection.Add(_provider.Object, _queues);
82 |
83 | IPersistentJobQueueProvider provider1 = collection.GetProvider("default");
84 | IPersistentJobQueueProvider provider2 = collection.GetProvider("critical");
85 |
86 | Assert.NotSame(_defaultProvider.Object, provider1);
87 | Assert.Same(provider1, provider2);
88 | }
89 |
90 | private PersistentJobQueueProviderCollection CreateCollection()
91 | {
92 | return new PersistentJobQueueProviderCollection(_defaultProvider.Object);
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/tests/Hangfire.PostgreSql.Tests/PostgreSqlFetchedJobFacts.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using System.Linq;
4 | using System.Threading;
5 | using Dapper;
6 | using Hangfire.PostgreSql.Tests.Utils;
7 | using Xunit;
8 |
9 | namespace Hangfire.PostgreSql.Tests
10 | {
11 | public class PostgreSqlFetchedJobFacts
12 | {
13 | private const string JobId = "id";
14 | private const string Queue = "queue";
15 | private DateTime _fetchedAt = DateTime.UtcNow;
16 |
17 | private readonly PostgreSqlStorage _storage;
18 |
19 | public PostgreSqlFetchedJobFacts()
20 | {
21 | _storage = new PostgreSqlStorage(ConnectionUtils.GetDefaultConnectionFactory());
22 | }
23 |
24 | [Fact]
25 | public void Ctor_ThrowsAnException_WhenStorageIsNull()
26 | {
27 | ArgumentNullException exception = Assert.Throws(() => new PostgreSqlFetchedJob(null, 1, JobId, Queue, _fetchedAt));
28 |
29 | Assert.Equal("storage", exception.ParamName);
30 | }
31 |
32 | [Fact]
33 | public void Ctor_ThrowsAnException_WhenJobIdIsNull()
34 | {
35 | ArgumentNullException exception = Assert.Throws(() => new PostgreSqlFetchedJob(_storage, 1, null, Queue, _fetchedAt));
36 |
37 | Assert.Equal("jobId", exception.ParamName);
38 | }
39 |
40 | [Fact]
41 | public void Ctor_ThrowsAnException_WhenQueueIsNull()
42 | {
43 | ArgumentNullException exception = Assert.Throws(() => new PostgreSqlFetchedJob(_storage, 1, JobId, null, _fetchedAt));
44 |
45 | Assert.Equal("queue", exception.ParamName);
46 | }
47 |
48 | [Fact]
49 | public void Ctor_ThrowsAnException_WhenFetchedAtIsNull()
50 | {
51 | ArgumentNullException exception = Assert.Throws(() => new PostgreSqlFetchedJob(_storage, 1, JobId, Queue, null));
52 | Assert.Equal("fetchedAt", exception.ParamName);
53 | }
54 |
55 | [Fact]
56 | public void Ctor_CorrectlySets_AllInstanceProperties()
57 | {
58 | PostgreSqlFetchedJob fetchedJob = new(_storage, 1, JobId, Queue, _fetchedAt);
59 |
60 | Assert.Equal(1, fetchedJob.Id);
61 | Assert.Equal(JobId, fetchedJob.JobId);
62 | Assert.Equal(Queue, fetchedJob.Queue);
63 | Assert.Equal(_fetchedAt, fetchedJob.FetchedAt);
64 | }
65 |
66 | [Fact]
67 | [CleanDatabase]
68 | public void RemoveFromQueue_ReallyDeletesTheJobFromTheQueue()
69 | {
70 | // Arrange
71 | long id = CreateJobQueueRecord(_storage, "1", "default", _fetchedAt);
72 | PostgreSqlFetchedJob processingJob = new(_storage, id, "1", "default", _fetchedAt);
73 |
74 | // Act
75 | processingJob.RemoveFromQueue();
76 |
77 | // Assert
78 | long count = _storage.UseConnection(null, connection =>
79 | connection.QuerySingle($@"SELECT COUNT(*) FROM ""{GetSchemaName()}"".""jobqueue"""));
80 | Assert.Equal(0, count);
81 | }
82 |
83 | [Fact]
84 | [CleanDatabase]
85 | public void RemoveFromQueue_DoesNotDelete_UnrelatedJobs()
86 | {
87 | // Arrange
88 | CreateJobQueueRecord(_storage, "1", "default", _fetchedAt);
89 | CreateJobQueueRecord(_storage, "1", "critical", _fetchedAt);
90 | CreateJobQueueRecord(_storage, "2", "default", _fetchedAt);
91 |
92 | PostgreSqlFetchedJob fetchedJob = new PostgreSqlFetchedJob(_storage, 999, "1", "default", _fetchedAt);
93 |
94 | // Act
95 | fetchedJob.RemoveFromQueue();
96 |
97 | // Assert
98 | long count = _storage.UseConnection(null, connection =>
99 | connection.QuerySingle($@"SELECT COUNT(*) FROM ""{GetSchemaName()}"".""jobqueue"""));
100 | Assert.Equal(3, count);
101 | }
102 |
103 | [Fact]
104 | [CleanDatabase]
105 | public void Requeue_SetsFetchedAtValueToNull()
106 | {
107 | // Arrange
108 | long id = CreateJobQueueRecord(_storage, "1", "default", _fetchedAt);
109 | PostgreSqlFetchedJob processingJob = new(_storage, id, "1", "default", _fetchedAt);
110 |
111 | // Act
112 | processingJob.Requeue();
113 |
114 | // Assert
115 | dynamic record = _storage.UseConnection(null, connection =>
116 | connection.Query($@"SELECT * FROM ""{GetSchemaName()}"".""jobqueue""").Single());
117 | Assert.Null(record.fetchedat);
118 | }
119 |
120 | [Fact]
121 | [CleanDatabase]
122 | public void Timer_UpdatesFetchedAtColumn()
123 | {
124 | _storage.UseConnection(null, connection => {
125 | // Arrange
126 | var fetchedAt = DateTime.UtcNow.AddMinutes(-5);
127 | long id = CreateJobQueueRecord(_storage, "1", "default", fetchedAt);
128 | using (var processingJob = new PostgreSqlFetchedJob(_storage, id, "1", "default", fetchedAt))
129 | {
130 | processingJob.DisposeTimer();
131 | Thread.Sleep(TimeSpan.FromSeconds(10));
132 | processingJob.ExecuteKeepAliveQueryIfRequired();
133 |
134 | dynamic record = connection.Query($@"SELECT * FROM ""{GetSchemaName()}"".""jobqueue""").Single();
135 |
136 | Assert.NotNull(processingJob.FetchedAt);
137 | Assert.Equal(processingJob.FetchedAt, record.fetchedat);
138 | DateTime now = DateTime.UtcNow;
139 | Assert.True(now.AddSeconds(-5) < record.fetchedat, (now - record.fetchedat).ToString());
140 | }
141 | });
142 | }
143 |
144 | [Fact]
145 | [CleanDatabase]
146 | public void RemoveFromQueue_AfterTimer_RemovesJobFromTheQueue()
147 | {
148 | _storage.UseConnection(null, connection => {
149 | // Arrange
150 | long id = CreateJobQueueRecord(_storage, "1", "default", _fetchedAt);
151 | using (PostgreSqlFetchedJob processingJob = new PostgreSqlFetchedJob(_storage, id, "1", "default", _fetchedAt))
152 | {
153 | Thread.Sleep(TimeSpan.FromSeconds(10));
154 | processingJob.DisposeTimer();
155 |
156 | // Act
157 | processingJob.RemoveFromQueue();
158 |
159 | // Assert
160 | int count = connection.Query($@"SELECT count(*) FROM ""{GetSchemaName()}"".""jobqueue""").Single();
161 | Assert.Equal(0, count);
162 | }
163 | });
164 | }
165 |
166 | [Fact]
167 | [CleanDatabase]
168 | public void RequeueQueue_AfterTimer_SetsFetchedAtValueToNull()
169 | {
170 | _storage.UseConnection(null, connection => {
171 | // Arrange
172 | long id = CreateJobQueueRecord(_storage, "1", "default", _fetchedAt);
173 | using (var processingJob = new PostgreSqlFetchedJob(_storage, id, "1", "default", _fetchedAt))
174 | {
175 | Thread.Sleep(TimeSpan.FromSeconds(10));
176 | processingJob.DisposeTimer();
177 |
178 | // Act
179 | processingJob.Requeue();
180 |
181 | // Assert
182 | dynamic record = connection.Query($@"SELECT * FROM ""{GetSchemaName()}"".""jobqueue""").Single();
183 | Assert.Null(record.fetchedat);
184 | }
185 | });
186 | }
187 |
188 | [Fact]
189 | [CleanDatabase]
190 | public void Dispose_SetsFetchedAtValueToNull_IfThereWereNoCallsToComplete()
191 | {
192 | // Arrange
193 | long id = CreateJobQueueRecord(_storage, "1", "default", _fetchedAt);
194 | PostgreSqlFetchedJob processingJob = new(_storage, id, "1", "default", _fetchedAt);
195 |
196 | // Act
197 | processingJob.Dispose();
198 |
199 | // Assert
200 | dynamic record = _storage.UseConnection(null, connection =>
201 | connection.Query($@"SELECT * FROM ""{GetSchemaName()}"".""jobqueue""").Single());
202 | Assert.Null(record.fetchedat);
203 | }
204 |
205 | private static long CreateJobQueueRecord(PostgreSqlStorage storage, string jobId, string queue, DateTime? fetchedAt)
206 | {
207 | string arrangeSql = $@"
208 | INSERT INTO ""{GetSchemaName()}"".""jobqueue"" (""jobid"", ""queue"", ""fetchedat"")
209 | VALUES (@Id, @Queue, @FetchedAt) RETURNING ""id""
210 | ";
211 |
212 | return
213 | storage.UseConnection(null, connection =>
214 | connection.QuerySingle(arrangeSql,
215 | new { Id = Convert.ToInt64(jobId, CultureInfo.InvariantCulture), Queue = queue, FetchedAt = fetchedAt }));
216 | }
217 |
218 | private static string GetSchemaName()
219 | {
220 | return ConnectionUtils.GetSchemaName();
221 | }
222 | }
223 | }
224 |
--------------------------------------------------------------------------------
/tests/Hangfire.PostgreSql.Tests/PostgreSqlInstallerFacts.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using System.Linq;
4 | using Dapper;
5 | using Hangfire.PostgreSql.Tests.Utils;
6 | using Npgsql;
7 | using Xunit;
8 |
9 | namespace Hangfire.PostgreSql.Tests
10 | {
11 | public class PostgreSqlInstallerFacts
12 | {
13 | [Fact]
14 | public void InstallingSchemaUpdatesVersionAndShouldNotThrowAnException()
15 | {
16 | Exception ex = Record.Exception(() => {
17 | UseConnection(connection => {
18 | string schemaName = "hangfire_tests_" + Guid.NewGuid().ToString().Replace("-", "_").ToLower(CultureInfo.InvariantCulture);
19 |
20 | PostgreSqlObjectsInstaller.Install(connection, schemaName);
21 |
22 | int lastVersion = connection.Query($@"SELECT version FROM ""{schemaName}"".""schema""").Single();
23 | Assert.Equal(23, lastVersion);
24 |
25 | connection.Execute($@"DROP SCHEMA ""{schemaName}"" CASCADE;");
26 | });
27 | });
28 |
29 | Assert.Null(ex);
30 | }
31 |
32 | [Fact]
33 | public void InstallingSchemaWithCapitalsUpdatesVersionAndShouldNotThrowAnException()
34 | {
35 | Exception ex = Record.Exception(() => {
36 | UseConnection(connection => {
37 | string schemaName = "Hangfire_Tests_" + Guid.NewGuid().ToString().Replace("-", "_").ToLower(CultureInfo.InvariantCulture);
38 |
39 | PostgreSqlObjectsInstaller.Install(connection, schemaName);
40 |
41 | int lastVersion = connection.Query($@"SELECT version FROM ""{schemaName}"".""schema""").Single();
42 | Assert.Equal(23, lastVersion);
43 |
44 | connection.Execute($@"DROP SCHEMA ""{schemaName}"" CASCADE;");
45 | });
46 | });
47 |
48 | Assert.Null(ex);
49 | }
50 |
51 | private static void UseConnection(Action action)
52 | {
53 | using NpgsqlConnection connection = ConnectionUtils.CreateConnection();
54 | action(connection);
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/tests/Hangfire.PostgreSql.Tests/PostgreSqlMonitoringApiFacts.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Globalization;
4 | using System.Linq;
5 | using Dapper;
6 | using Hangfire.Common;
7 | using Hangfire.PostgreSql.Tests.Utils;
8 | using Hangfire.States;
9 | using Hangfire.Storage;
10 | using Hangfire.Storage.Monitoring;
11 | using Moq;
12 | using Npgsql;
13 | using Xunit;
14 |
15 | namespace Hangfire.PostgreSql.Tests
16 | {
17 | public class PostgreSqlMonitoringApiFacts : IClassFixture
18 | {
19 | private readonly PostgreSqlStorageFixture _fixture;
20 |
21 | public PostgreSqlMonitoringApiFacts(PostgreSqlStorageFixture fixture)
22 | {
23 | _fixture = fixture;
24 | }
25 |
26 | [Fact]
27 | [CleanDatabase]
28 | public void GetJobs_MixedCasing_ReturnsJob()
29 | {
30 | string arrangeSql = $@"
31 | INSERT INTO ""{ConnectionUtils.GetSchemaName()}"".""job""(""invocationdata"", ""arguments"", ""createdat"")
32 | VALUES (@InvocationData, @Arguments, NOW()) RETURNING ""id""";
33 |
34 | Job job = Job.FromExpression(() => SampleMethod("Hello"));
35 | InvocationData invocationData = InvocationData.SerializeJob(job);
36 |
37 | UseConnection(connection => {
38 | long jobId = connection.QuerySingle(arrangeSql,
39 | new {
40 | InvocationData = new JsonParameter(SerializationHelper.Serialize(invocationData)),
41 | Arguments = new JsonParameter(invocationData.Arguments, JsonParameter.ValueType.Array),
42 | });
43 |
44 | Mock state = new();
45 | state.Setup(x => x.Name).Returns(SucceededState.StateName);
46 | state.Setup(x => x.SerializeData())
47 | .Returns(new Dictionary {
48 | { "SUCCEEDEDAT", "2018-05-03T13:28:18.3939693Z" },
49 | { "PerformanceDuration", "53" },
50 | { "latency", "6730" },
51 | });
52 |
53 | Commit(connection, x => x.SetJobState(jobId.ToString(CultureInfo.InvariantCulture), state.Object));
54 |
55 | IMonitoringApi monitoringApi = _fixture.Storage.GetMonitoringApi();
56 | JobList jobs = monitoringApi.SucceededJobs(0, 10);
57 |
58 | Assert.NotNull(jobs);
59 | });
60 | }
61 |
62 | [Fact]
63 | [CleanDatabase]
64 | public void HourlySucceededJobs_ReturnsAggregatedStats()
65 | {
66 | DateTime now = DateTime.UtcNow;
67 | string schemaName = ConnectionUtils.GetSchemaName();
68 | string key = $"stats:succeeded:{now.ToString("yyyy-MM-dd-HH", CultureInfo.InvariantCulture)}";
69 | string arrangeSql =
70 | $"""
71 | BEGIN;
72 | INSERT INTO "{schemaName}"."counter"("key", "value")
73 | VALUES (@Key, 5);
74 | INSERT INTO "{schemaName}"."aggregatedcounter"("key", "value")
75 | VALUES (@Key, 7);
76 | COMMIT;
77 | """;
78 | UseConnection(connection => {
79 | connection.Execute(arrangeSql, new { Key = key });
80 |
81 | IMonitoringApi monitoringApi = _fixture.Storage.GetMonitoringApi();
82 | IDictionary stats = monitoringApi.HourlySucceededJobs();
83 | Assert.Equal(24, stats.Count);
84 |
85 | long actualCounter = Assert.Single(stats.Where(x => x.Key.Hour == now.Hour).Select(x => x.Value));
86 | Assert.Equal(12, actualCounter);
87 | });
88 | }
89 |
90 | private void UseConnection(Action action)
91 | {
92 | PostgreSqlStorage storage = _fixture.SafeInit();
93 | action(storage.CreateAndOpenConnection());
94 | }
95 |
96 | private void Commit(
97 | NpgsqlConnection connection,
98 | Action action)
99 | {
100 | PostgreSqlStorage storage = _fixture.SafeInit();
101 | using PostgreSqlWriteOnlyTransaction transaction = new(storage, () => connection);
102 | action(transaction);
103 | transaction.Commit();
104 | }
105 |
106 | #pragma warning disable xUnit1013 // Public method should be marked as test
107 | public static void SampleMethod(string arg)
108 | #pragma warning restore xUnit1013 // Public method should be marked as test
109 | { }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/tests/Hangfire.PostgreSql.Tests/PostgreSqlStorageFacts.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Transactions;
5 | using Hangfire.PostgreSql.Factories;
6 | using Hangfire.PostgreSql.Tests.Utils;
7 | using Hangfire.Server;
8 | using Hangfire.Storage;
9 | using Npgsql;
10 | using Xunit;
11 |
12 | namespace Hangfire.PostgreSql.Tests
13 | {
14 | public class PostgreSqlStorageFacts
15 | {
16 | private readonly PostgreSqlStorageOptions _options;
17 |
18 | public PostgreSqlStorageFacts()
19 | {
20 | _options = new PostgreSqlStorageOptions { PrepareSchemaIfNecessary = false, EnableTransactionScopeEnlistment = true };
21 | }
22 |
23 | [Fact]
24 | [CleanDatabase]
25 | public void Ctor_CanCreateSqlServerStorage_WithExistingConnection()
26 | {
27 | NpgsqlConnection connection = ConnectionUtils.CreateConnection();
28 | PostgreSqlStorage storage = new(new ExistingNpgsqlConnectionFactory(connection, _options), _options);
29 |
30 | Assert.NotNull(storage);
31 | }
32 |
33 | [Fact]
34 | [CleanDatabase]
35 | public void Ctor_InitializesDefaultJobQueueProvider_AndPassesCorrectOptions()
36 | {
37 | PostgreSqlStorage storage = CreateStorage();
38 | PersistentJobQueueProviderCollection providers = storage.QueueProviders;
39 |
40 | PostgreSqlJobQueueProvider provider = (PostgreSqlJobQueueProvider)providers.GetProvider("default");
41 |
42 | Assert.Same(_options, provider.Options);
43 | }
44 |
45 | [Fact]
46 | [CleanDatabase]
47 | public void GetMonitoringApi_ReturnsNonNullInstance()
48 | {
49 | PostgreSqlStorage storage = CreateStorage();
50 | IMonitoringApi api = storage.GetMonitoringApi();
51 | Assert.NotNull(api);
52 | }
53 |
54 | [Fact]
55 | [CleanDatabase]
56 | public void GetComponents_ReturnsAllNeededComponents()
57 | {
58 | PostgreSqlStorage storage = CreateStorage();
59 |
60 | #pragma warning disable CS0618 // Type or member is obsolete
61 | IEnumerable components = storage.GetComponents();
62 | #pragma warning restore CS0618 // Type or member is obsolete
63 |
64 | Type[] componentTypes = components.Select(x => x.GetType()).ToArray();
65 | Assert.Contains(typeof(ExpirationManager), componentTypes);
66 | }
67 |
68 | [Fact]
69 | public void Ctor_ThrowsAnException_WhenConnectionFactoryIsNull()
70 | {
71 | ArgumentNullException exception = Assert.Throws(() => new PostgreSqlStorage(connectionFactory: null, new PostgreSqlStorageOptions()));
72 | Assert.Equal("connectionFactory", exception.ParamName);
73 | }
74 |
75 | [Fact]
76 | [CleanDatabase]
77 | public void Ctor_CanCreateSqlServerStorage_WithExistingConnectionFactory()
78 | {
79 | PostgreSqlStorage storage = new(new DefaultConnectionFactory(), _options);
80 | Assert.NotNull(storage);
81 | }
82 |
83 | [Fact]
84 | [CleanDatabase]
85 | public void CanCreateAndOpenConnection_WithExistingConnectionFactory()
86 | {
87 | PostgreSqlStorage storage = new(new DefaultConnectionFactory(), _options);
88 | NpgsqlConnection connection = storage.CreateAndOpenConnection();
89 | Assert.NotNull(connection);
90 | }
91 |
92 | [Fact]
93 | public void CreateAndOpenConnection_ThrowsAnException_WithExistingConnectionFactoryAndInvalidOptions()
94 | {
95 | PostgreSqlStorageOptions option = new() {
96 | EnableTransactionScopeEnlistment = false,
97 | PrepareSchemaIfNecessary = false,
98 | };
99 | Assert.Throws(() => new PostgreSqlStorage(ConnectionUtils.GetDefaultConnectionFactory(option), option));
100 | }
101 |
102 | [Fact]
103 | public void CanUseTransaction_WithDifferentTransactionIsolationLevel()
104 | {
105 | using TransactionScope scope = new(TransactionScopeOption.Required,
106 | new TransactionOptions() { IsolationLevel = IsolationLevel.Serializable });
107 |
108 | PostgreSqlStorage storage = new(new DefaultConnectionFactory(), _options);
109 | NpgsqlConnection connection = storage.CreateAndOpenConnection();
110 |
111 | bool success = storage.UseTransaction(connection, (_, _) => true);
112 |
113 | Assert.True(success);
114 | }
115 |
116 | [Fact]
117 | public void HasFeature_ThrowsAnException_WhenFeatureIsNull()
118 | {
119 | ArgumentNullException aex = Assert.Throws(() => new PostgreSqlStorage(new DefaultConnectionFactory(), _options).HasFeature(null));
120 | Assert.Equal("featureId", aex.ParamName);
121 | }
122 |
123 | [Theory]
124 | [InlineData("Job.Queue", true)] // JobStorageFeatures.JobQueueProperty
125 | [InlineData("Connection.BatchedGetFirstByLowestScoreFromSet", true)] // JobStorageFeatures.Connection.BatchedGetFirstByLowest
126 | [InlineData("", false)]
127 | [InlineData("Unsupported", false)]
128 | public void HasFeature_ReturnsCorrectValues(string featureName, bool expected)
129 | {
130 | PostgreSqlStorage storage = new(new DefaultConnectionFactory(), _options);
131 | bool actual = storage.HasFeature(featureName);
132 | Assert.Equal(expected, actual);
133 | }
134 |
135 | private PostgreSqlStorage CreateStorage()
136 | {
137 | return new PostgreSqlStorage(ConnectionUtils.GetDefaultConnectionFactory(), _options);
138 | }
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/tests/Hangfire.PostgreSql.Tests/PostgreSqlStorageOptionsFacts.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Xunit;
3 |
4 | namespace Hangfire.PostgreSql.Tests
5 | {
6 | public class PostgreSqlStorageOptionsFacts
7 | {
8 | [Fact]
9 | public void Ctor_SetsTheDefaultOptions()
10 | {
11 | PostgreSqlStorageOptions options = new();
12 |
13 | Assert.True(options.QueuePollInterval > TimeSpan.Zero);
14 | Assert.True(options.InvisibilityTimeout > TimeSpan.Zero);
15 | Assert.True(options.DistributedLockTimeout > TimeSpan.Zero);
16 | Assert.True(options.PrepareSchemaIfNecessary);
17 | }
18 |
19 | [Fact]
20 | public void Set_QueuePollInterval_ShouldThrowAnException_WhenGivenIntervalIsTooLow()
21 | {
22 | PostgreSqlStorageOptions options = new();
23 | Assert.Throws(() => options.QueuePollInterval = TimeSpan.FromMilliseconds(10));
24 | }
25 |
26 | [Fact]
27 | public void Set_QueuePollInterval_SetsTheValue_WhenGivenIntervalIsTooLow_ButIgnored()
28 | {
29 | PostgreSqlStorageOptions options = new() {
30 | AllowUnsafeValues = true,
31 | QueuePollInterval = TimeSpan.FromMilliseconds(10),
32 | };
33 | Assert.Equal(TimeSpan.FromMilliseconds(10), options.QueuePollInterval);
34 | }
35 |
36 | [Fact]
37 | public void Set_QueuePollInterval_ShouldThrowAnException_WhenGivenIntervalIsEqualToZero_EvenIfIgnored()
38 | {
39 | PostgreSqlStorageOptions options = new() { AllowUnsafeValues = true };
40 | Assert.Throws(() => options.QueuePollInterval = TimeSpan.Zero);
41 | }
42 |
43 | [Fact]
44 | public void Set_QueuePollInterval_SetsTheValue()
45 | {
46 | PostgreSqlStorageOptions options = new();
47 | options.QueuePollInterval = TimeSpan.FromSeconds(1);
48 | Assert.Equal(TimeSpan.FromSeconds(1), options.QueuePollInterval);
49 | }
50 |
51 | [Fact]
52 | public void Set_InvisibilityTimeout_ShouldThrowAnException_WhenGivenIntervalIsEqualToZero()
53 | {
54 | PostgreSqlStorageOptions options = new();
55 | Assert.Throws(() => options.InvisibilityTimeout = TimeSpan.Zero);
56 | }
57 |
58 | [Fact]
59 | public void Set_InvisibilityTimeout_ShouldThrowAnException_WhenGivenIntervalIsNegative()
60 | {
61 | PostgreSqlStorageOptions options = new();
62 | Assert.Throws(() => options.InvisibilityTimeout = TimeSpan.FromSeconds(-1));
63 | }
64 |
65 | [Fact]
66 | public void Set_InvisibilityTimeout_SetsTheValue()
67 | {
68 | PostgreSqlStorageOptions options = new();
69 | options.InvisibilityTimeout = TimeSpan.FromSeconds(1);
70 | Assert.Equal(TimeSpan.FromSeconds(1), options.InvisibilityTimeout);
71 | }
72 |
73 | [Fact]
74 | public void Set_DistributedLockTimeout_ShouldThrowAnException_WhenGivenIntervalIsEqualToZero()
75 | {
76 | PostgreSqlStorageOptions options = new();
77 | Assert.Throws(() => options.DistributedLockTimeout = TimeSpan.Zero);
78 | }
79 |
80 | [Fact]
81 | public void Set_DistributedLockTimeout_ShouldThrowAnException_WhenGivenIntervalIsNegative()
82 | {
83 | PostgreSqlStorageOptions options = new();
84 | Assert.Throws(() => options.DistributedLockTimeout = TimeSpan.FromSeconds(-1));
85 | }
86 |
87 | [Fact]
88 | public void Set_DistributedLockTimeout_SetsTheValue()
89 | {
90 | PostgreSqlStorageOptions options = new();
91 | options.DistributedLockTimeout = TimeSpan.FromSeconds(1);
92 | Assert.Equal(TimeSpan.FromSeconds(1), options.DistributedLockTimeout);
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/tests/Hangfire.PostgreSql.Tests/Scripts/Clean.sql:
--------------------------------------------------------------------------------
1 | SET search_path = 'hangfire';
2 |
3 | DELETE FROM hangfire."aggregatedcounter";
4 | DELETE FROM hangfire."counter";
5 | DELETE FROM hangfire."hash";
6 | DELETE FROM hangfire."job";
7 | DELETE FROM hangfire."jobparameter";
8 | DELETE FROM hangfire."jobqueue";
9 | DELETE FROM hangfire."list";
10 | DELETE FROM hangfire."lock";
11 | DELETE FROM hangfire."server";
12 | DELETE FROM hangfire."set";
13 | DELETE FROM hangfire."state";
--------------------------------------------------------------------------------
/tests/Hangfire.PostgreSql.Tests/Utils/CleanDatabaseAttribute.cs:
--------------------------------------------------------------------------------
1 | using System.Data;
2 | using System.Reflection;
3 | using System.Threading;
4 | using Dapper;
5 | using Npgsql;
6 | using Xunit.Sdk;
7 |
8 | namespace Hangfire.PostgreSql.Tests.Utils
9 | {
10 | public class CleanDatabaseAttribute : BeforeAfterTestAttribute
11 | {
12 | private static readonly object _globalLock = new();
13 | private static bool _sqlObjectInstalled;
14 |
15 | public override void Before(MethodInfo methodUnderTest)
16 | {
17 | Monitor.Enter(_globalLock);
18 |
19 | if (!_sqlObjectInstalled)
20 | {
21 | RecreateSchemaAndInstallObjects();
22 | _sqlObjectInstalled = true;
23 | }
24 |
25 | CleanTables();
26 | }
27 |
28 | public override void After(MethodInfo methodUnderTest)
29 | {
30 | try { }
31 | finally
32 | {
33 | Monitor.Exit(_globalLock);
34 | }
35 | }
36 |
37 | private static void RecreateSchemaAndInstallObjects()
38 | {
39 | using NpgsqlConnection masterConnection = ConnectionUtils.CreateMasterConnection();
40 | bool databaseExists = masterConnection.QuerySingleOrDefault($@"SELECT true :: boolean FROM pg_database WHERE datname = @DatabaseName;",
41 | new {
42 | DatabaseName = ConnectionUtils.GetDatabaseName(),
43 | }) ?? false;
44 |
45 | if (!databaseExists)
46 | {
47 | masterConnection.Execute($@"CREATE DATABASE ""{ConnectionUtils.GetDatabaseName()}""");
48 | }
49 |
50 | using NpgsqlConnection connection = ConnectionUtils.CreateConnection();
51 | if (connection.State == ConnectionState.Closed)
52 | {
53 | connection.Open();
54 | }
55 |
56 | PostgreSqlObjectsInstaller.Install(connection);
57 | PostgreSqlTestObjectsInitializer.CleanTables(connection);
58 | }
59 |
60 | private static void CleanTables()
61 | {
62 | using NpgsqlConnection connection = ConnectionUtils.CreateConnection();
63 | PostgreSqlTestObjectsInitializer.CleanTables(connection);
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/tests/Hangfire.PostgreSql.Tests/Utils/ConnectionUtils.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using Hangfire.Annotations;
4 | using Hangfire.PostgreSql.Factories;
5 | using Npgsql;
6 |
7 | namespace Hangfire.PostgreSql.Tests.Utils
8 | {
9 | public static class ConnectionUtils
10 | {
11 | private const string DatabaseVariable = "Hangfire_PostgreSql_DatabaseName";
12 | private const string SchemaVariable = "Hangfire_PostgreSql_SchemaName";
13 |
14 | private const string ConnectionStringTemplateVariable = "Hangfire_PostgreSql_ConnectionStringTemplate";
15 |
16 | private const string MasterDatabaseName = "postgres";
17 | private const string DefaultDatabaseName = @"hangfire_tests";
18 | private const string DefaultSchemaName = @"hangfire";
19 |
20 | private const string DefaultConnectionStringTemplate = @"Server=127.0.0.1;Port=5432;Database=postgres;User Id=postgres;Password=password;";
21 |
22 | public static string GetDatabaseName()
23 | {
24 | return Environment.GetEnvironmentVariable(DatabaseVariable) ?? DefaultDatabaseName;
25 | }
26 |
27 | public static string GetSchemaName()
28 | {
29 | return Environment.GetEnvironmentVariable(SchemaVariable) ?? DefaultSchemaName;
30 | }
31 |
32 | public static string GetMasterConnectionString()
33 | {
34 | return string.Format(CultureInfo.InvariantCulture, GetConnectionStringTemplate(), MasterDatabaseName);
35 | }
36 |
37 | public static string GetConnectionString()
38 | {
39 | return string.Format(CultureInfo.InvariantCulture, GetConnectionStringTemplate(), GetDatabaseName());
40 | }
41 |
42 | public static NpgsqlConnectionFactory GetDefaultConnectionFactory([CanBeNull] PostgreSqlStorageOptions options = null)
43 | {
44 | return new NpgsqlConnectionFactory(GetConnectionString(), options ?? new PostgreSqlStorageOptions());
45 | }
46 |
47 | private static string GetConnectionStringTemplate()
48 | {
49 | return Environment.GetEnvironmentVariable(ConnectionStringTemplateVariable)
50 | ?? DefaultConnectionStringTemplate;
51 | }
52 |
53 | public static NpgsqlConnection CreateConnection()
54 | {
55 | NpgsqlConnectionStringBuilder csb = new(GetConnectionString());
56 |
57 | NpgsqlConnection connection = new() {
58 | ConnectionString = csb.ToString(),
59 | };
60 | connection.Open();
61 |
62 | return connection;
63 | }
64 |
65 | public static NpgsqlConnection CreateMasterConnection()
66 | {
67 | NpgsqlConnectionStringBuilder csb = new(GetMasterConnectionString());
68 |
69 | NpgsqlConnection connection = new() {
70 | ConnectionString = csb.ToString(),
71 | };
72 | connection.Open();
73 |
74 | return connection;
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/tests/Hangfire.PostgreSql.Tests/Utils/DefaultConnectionFactory.cs:
--------------------------------------------------------------------------------
1 | using Npgsql;
2 |
3 | namespace Hangfire.PostgreSql.Tests.Utils
4 | {
5 | public class DefaultConnectionFactory : IConnectionFactory
6 | {
7 | ///
8 | /// Get or create NpgsqlConnection
9 | ///
10 | public NpgsqlConnection GetOrCreateConnection()
11 | {
12 | return ConnectionUtils.CreateConnection();
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/tests/Hangfire.PostgreSql.Tests/Utils/Helper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Data;
3 | using System.Globalization;
4 | using Dapper;
5 | using Hangfire.PostgreSql.Tests.Entities;
6 |
7 | namespace Hangfire.PostgreSql.Tests.Utils
8 | {
9 | public static class Helper
10 | {
11 | public static TestJob GetTestJob(IDbConnection connection, string schemaName, string jobId)
12 | {
13 | return connection
14 | .QuerySingle($@"SELECT ""id"" ""Id"", ""invocationdata"" ""InvocationData"", ""arguments"" ""Arguments"", ""expireat"" ""ExpireAt"", ""statename"" ""StateName"", ""stateid"" ""StateId"", ""createdat"" ""CreatedAt"" FROM ""{schemaName}"".""job"" WHERE ""id"" = @Id OR @Id = -1",
15 | new { Id = Convert.ToInt64(jobId, CultureInfo.InvariantCulture) });
16 | }
17 |
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/tests/Hangfire.PostgreSql.Tests/Utils/PostgreSqlStorageExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace Hangfire.PostgreSql.Tests.Utils
2 | {
3 | internal static class PostgreSqlStorageExtensions
4 | {
5 | public static PostgreSqlConnection GetStorageConnection(this PostgreSqlStorage storage)
6 | {
7 | return storage.GetConnection() as PostgreSqlConnection;
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/tests/Hangfire.PostgreSql.Tests/Utils/PostgreSqlStorageFixture.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Hangfire.PostgreSql.Factories;
3 | using Moq;
4 | using Npgsql;
5 |
6 | namespace Hangfire.PostgreSql.Tests.Utils
7 | {
8 | public class PostgreSqlStorageFixture : IDisposable
9 | {
10 | private readonly PostgreSqlStorageOptions _storageOptions;
11 | private bool _initialized;
12 | private NpgsqlConnection _mainConnection;
13 |
14 | public PostgreSqlStorageFixture()
15 | {
16 | PersistentJobQueueMock = new Mock();
17 |
18 | Mock provider = new();
19 | provider.Setup(x => x.GetJobQueue())
20 | .Returns(PersistentJobQueueMock.Object);
21 |
22 | PersistentJobQueueProviderCollection = new PersistentJobQueueProviderCollection(provider.Object);
23 |
24 | _storageOptions = new PostgreSqlStorageOptions {
25 | SchemaName = ConnectionUtils.GetSchemaName(),
26 | EnableTransactionScopeEnlistment = true,
27 | };
28 | }
29 |
30 | public Mock PersistentJobQueueMock { get; }
31 |
32 | public PersistentJobQueueProviderCollection PersistentJobQueueProviderCollection { get; }
33 |
34 | public PostgreSqlStorage Storage { get; private set; }
35 | public NpgsqlConnection MainConnection => _mainConnection ?? (_mainConnection = ConnectionUtils.CreateConnection());
36 |
37 | public void Dispose()
38 | {
39 | _mainConnection?.Dispose();
40 | _mainConnection = null;
41 | }
42 |
43 | public void SetupOptions(Action storageOptionsConfigure)
44 | {
45 | storageOptionsConfigure(_storageOptions);
46 | }
47 |
48 | public PostgreSqlStorage SafeInit(NpgsqlConnection connection = null)
49 | {
50 | return _initialized
51 | ? Storage
52 | : ForceInit(connection);
53 | }
54 |
55 | public PostgreSqlStorage ForceInit(NpgsqlConnection connection = null)
56 | {
57 | Storage = new PostgreSqlStorage(new ExistingNpgsqlConnectionFactory(connection ?? MainConnection, _storageOptions), _storageOptions) {
58 | QueueProviders = PersistentJobQueueProviderCollection,
59 | };
60 | _initialized = true;
61 | return Storage;
62 | }
63 |
64 | public void SafeInit(PostgreSqlStorageOptions options,
65 | PersistentJobQueueProviderCollection jobQueueProviderCollection = null,
66 | NpgsqlConnection connection = null)
67 | {
68 | if (!_initialized)
69 | {
70 | ForceInit(options, jobQueueProviderCollection, connection);
71 | return;
72 | }
73 |
74 | Storage.QueueProviders = jobQueueProviderCollection;
75 | }
76 |
77 | public void ForceInit(PostgreSqlStorageOptions options,
78 | PersistentJobQueueProviderCollection jobQueueProviderCollection = null,
79 | NpgsqlConnection connection = null)
80 | {
81 | Storage = new PostgreSqlStorage(new ExistingNpgsqlConnectionFactory(connection ?? MainConnection, options), options) {
82 | QueueProviders = jobQueueProviderCollection,
83 | };
84 | _initialized = true;
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/tests/Hangfire.PostgreSql.Tests/Utils/PostgreSqlTestObjectsInitializer.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.PostgreSql.
2 | // Copyright © 2014 Frank Hommers .
3 | //
4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.PostgreSql. If not, see .
16 | //
17 | // This work is based on the work of Sergey Odinokov, author of
18 | // Hangfire.
19 | //
20 | // Special thanks goes to him.
21 |
22 | using System;
23 | using System.Data;
24 | using System.IO;
25 | using System.Reflection;
26 | using Npgsql;
27 |
28 | namespace Hangfire.PostgreSql.Tests.Utils
29 | {
30 | internal static class PostgreSqlTestObjectsInitializer
31 | {
32 | public static void CleanTables(NpgsqlConnection connection)
33 | {
34 | if (connection == null) throw new ArgumentNullException(nameof(connection));
35 |
36 | string script = GetStringResource(typeof(PostgreSqlTestObjectsInitializer).GetTypeInfo().Assembly,
37 | "Hangfire.PostgreSql.Tests.Scripts.Clean.sql").Replace("'hangfire'", $"'{ConnectionUtils.GetSchemaName()}'");
38 |
39 | using NpgsqlTransaction transaction = connection.BeginTransaction(IsolationLevel.Serializable);
40 | using NpgsqlCommand command = new(script, connection, transaction);
41 | command.CommandTimeout = 120;
42 | command.ExecuteNonQuery();
43 | transaction.Commit();
44 | }
45 |
46 | private static string GetStringResource(Assembly assembly, string resourceName)
47 | {
48 | using Stream stream = assembly.GetManifestResourceStream(resourceName);
49 | if (stream == null)
50 | {
51 | throw new InvalidOperationException($"Requested resource '{resourceName}' was not found in the assembly '{assembly}'.");
52 | }
53 |
54 | using StreamReader reader = new(stream);
55 | return reader.ReadToEnd();
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------