| @@ -13,6 +13,7 @@ watch_dirs = [ | |||||
| "$WORKDIR/options", | "$WORKDIR/options", | ||||
| "$WORKDIR/public", | "$WORKDIR/public", | ||||
| "$WORKDIR/custom", | "$WORKDIR/custom", | ||||
| "$WORKDIR/web_src", | |||||
| ] # Directories to watch | ] # Directories to watch | ||||
| watch_exts = [".go", ".ini", ".less"] # Extensions to watch | watch_exts = [".go", ".ini", ".less"] # Extensions to watch | ||||
| env_files = [] # Load env vars from files | env_files = [] # Load env vars from files | ||||
| @@ -3,6 +3,7 @@ root: true | |||||
| extends: | extends: | ||||
| - eslint-config-airbnb-base | - eslint-config-airbnb-base | ||||
| - eslint:recommended | - eslint:recommended | ||||
| - plugin:vue/recommended | |||||
| ignorePatterns: | ignorePatterns: | ||||
| - /web_src/js/vendor | - /web_src/js/vendor | ||||
| @@ -1 +0,0 @@ | |||||
| nodejs 12.14.0 | |||||
| @@ -753,6 +753,8 @@ MINIO_LOCATION = us-east-1 | |||||
| MINIO_BASE_PATH = attachments/ | MINIO_BASE_PATH = attachments/ | ||||
| ; Minio enabled ssl only available when STORE_TYPE is `minio` | ; Minio enabled ssl only available when STORE_TYPE is `minio` | ||||
| MINIO_USE_SSL = false | MINIO_USE_SSL = false | ||||
| ; real Minio storage path | |||||
| MINIO_REAL_PATH = /mnt/test/minio/data/ | |||||
| [time] | [time] | ||||
| ; Specifies the format for fully outputted dates. Defaults to RFC1123 | ; Specifies the format for fully outputted dates. Defaults to RFC1123 | ||||
| @@ -76,6 +76,7 @@ require ( | |||||
| github.com/mgechev/revive v1.0.2 | github.com/mgechev/revive v1.0.2 | ||||
| github.com/microcosm-cc/bluemonday v1.0.3-0.20191119130333-0a75d7616912 | github.com/microcosm-cc/bluemonday v1.0.3-0.20191119130333-0a75d7616912 | ||||
| github.com/minio/minio-go v6.0.14+incompatible | github.com/minio/minio-go v6.0.14+incompatible | ||||
| github.com/minio/minio-go/v6 v6.0.57 | |||||
| github.com/mitchellh/go-homedir v1.1.0 | github.com/mitchellh/go-homedir v1.1.0 | ||||
| github.com/msteinert/pam v0.0.0-20151204160544-02ccfbfaf0cc | github.com/msteinert/pam v0.0.0-20151204160544-02ccfbfaf0cc | ||||
| github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5 | github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5 | ||||
| @@ -434,6 +434,8 @@ github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqx | |||||
| github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= | ||||
| github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= | ||||
| github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= | ||||
| github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= | |||||
| github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= | |||||
| github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= | ||||
| github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= | github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= | ||||
| github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= | ||||
| @@ -453,6 +455,8 @@ github.com/klauspost/compress v1.9.2 h1:LfVyl+ZlLlLDeQ/d2AqfGIIH4qEDu0Ed2S5GyhCW | |||||
| github.com/klauspost/compress v1.9.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= | github.com/klauspost/compress v1.9.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= | ||||
| github.com/klauspost/compress v1.10.2 h1:Znfn6hXZAHaLPNnlqUYRrBSReFHYybslgv4PTiyz6P0= | github.com/klauspost/compress v1.10.2 h1:Znfn6hXZAHaLPNnlqUYRrBSReFHYybslgv4PTiyz6P0= | ||||
| github.com/klauspost/compress v1.10.2/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= | github.com/klauspost/compress v1.10.2/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= | ||||
| github.com/klauspost/cpuid v1.2.3 h1:CCtW0xUnWGVINKvE/WWOYKdsPV6mawAtvQuSl8guwQs= | |||||
| github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= | |||||
| github.com/kljensen/snowball v0.6.0/go.mod h1:27N7E8fVU5H68RlUmnWwZCfxgt4POBJfENGMvNRhldw= | github.com/kljensen/snowball v0.6.0/go.mod h1:27N7E8fVU5H68RlUmnWwZCfxgt4POBJfENGMvNRhldw= | ||||
| github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= | ||||
| github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= | ||||
| @@ -512,15 +516,23 @@ github.com/mgechev/revive v1.0.2/go.mod h1:rb0dQy1LVAxW9SWy5R3LPUjevzUbUS316U5MF | |||||
| github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= | github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= | ||||
| github.com/microcosm-cc/bluemonday v1.0.3-0.20191119130333-0a75d7616912 h1:hJde9rA24hlTcAYSwJoXpDUyGtfKQ/jsofw+WaDqGrI= | github.com/microcosm-cc/bluemonday v1.0.3-0.20191119130333-0a75d7616912 h1:hJde9rA24hlTcAYSwJoXpDUyGtfKQ/jsofw+WaDqGrI= | ||||
| github.com/microcosm-cc/bluemonday v1.0.3-0.20191119130333-0a75d7616912/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w= | github.com/microcosm-cc/bluemonday v1.0.3-0.20191119130333-0a75d7616912/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w= | ||||
| github.com/minio/md5-simd v1.1.0 h1:QPfiOqlZH+Cj9teu0t9b1nTBfPbyTl16Of5MeuShdK4= | |||||
| github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw= | |||||
| github.com/minio/minio-go v6.0.14+incompatible h1:fnV+GD28LeqdN6vT2XdGKW8Qe/IfjJDswNVuni6km9o= | github.com/minio/minio-go v6.0.14+incompatible h1:fnV+GD28LeqdN6vT2XdGKW8Qe/IfjJDswNVuni6km9o= | ||||
| github.com/minio/minio-go v6.0.14+incompatible/go.mod h1:7guKYtitv8dktvNUGrhzmNlA5wrAABTQXCoesZdFQO8= | github.com/minio/minio-go v6.0.14+incompatible/go.mod h1:7guKYtitv8dktvNUGrhzmNlA5wrAABTQXCoesZdFQO8= | ||||
| github.com/minio/minio-go/v6 v6.0.57 h1:ixPkbKkyD7IhnluRgQpGSpHdpvNVaW6OD5R9IAO/9Tw= | |||||
| github.com/minio/minio-go/v6 v6.0.57/go.mod h1:5+R/nM9Pwrh0vqF+HbYYDQ84wdUFPyXHkrdT4AIkifM= | |||||
| github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU= | |||||
| github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= | |||||
| github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= | ||||
| github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= | ||||
| github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= | ||||
| github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= | ||||
| github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | ||||
| github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= | |||||
| github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | ||||
| github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= | ||||
| github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= | |||||
| github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= | ||||
| github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c h1:3wkDRdxK92dF+c1ke2dtj7ZzemFWBHB9plnJOtlwdFA= | github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c h1:3wkDRdxK92dF+c1ke2dtj7ZzemFWBHB9plnJOtlwdFA= | ||||
| github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM= | github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM= | ||||
| @@ -658,6 +670,7 @@ github.com/siddontang/go-snappy v0.0.0-20140704025258-d8f7bb82a96d/go.mod h1:vq0 | |||||
| github.com/siddontang/ledisdb v0.0.0-20190202134119-8ceb77e66a92/go.mod h1:mF1DpOSOUiJRMR+FDqaqu3EBqrybQtrDDszLUZ6oxPg= | github.com/siddontang/ledisdb v0.0.0-20190202134119-8ceb77e66a92/go.mod h1:mF1DpOSOUiJRMR+FDqaqu3EBqrybQtrDDszLUZ6oxPg= | ||||
| github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d/go.mod h1:AMEsy7v5z92TR1JKMkLLoaOQk++LVnOKL3ScbJ8GNGA= | github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d/go.mod h1:AMEsy7v5z92TR1JKMkLLoaOQk++LVnOKL3ScbJ8GNGA= | ||||
| github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= | ||||
| github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= | |||||
| github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= | ||||
| github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= | github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= | ||||
| github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w= | github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w= | ||||
| @@ -783,6 +796,7 @@ golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnf | |||||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||
| golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||
| golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||
| golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | |||||
| golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||
| golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||
| golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||
| @@ -879,6 +893,7 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h | |||||
| golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | |||||
| golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| @@ -22,7 +22,6 @@ import ( | |||||
| _ "code.gitea.io/gitea/modules/markup/markdown" | _ "code.gitea.io/gitea/modules/markup/markdown" | ||||
| _ "code.gitea.io/gitea/modules/markup/orgmode" | _ "code.gitea.io/gitea/modules/markup/orgmode" | ||||
| _ "code.gitea.io/gitea/modules/timer" | _ "code.gitea.io/gitea/modules/timer" | ||||
| _ "code.gitea.io/gitea/modules/worker" | |||||
| "github.com/urfave/cli" | "github.com/urfave/cli" | ||||
| ) | ) | ||||
| @@ -28,19 +28,19 @@ const ( | |||||
| // Attachment represent a attachment of issue/comment/release. | // Attachment represent a attachment of issue/comment/release. | ||||
| type Attachment struct { | type Attachment struct { | ||||
| ID int64 `xorm:"pk autoincr"` | |||||
| UUID string `xorm:"uuid UNIQUE"` | |||||
| IssueID int64 `xorm:"INDEX"` | |||||
| DatasetID int64 `xorm:"INDEX DEFAULT 0"` | |||||
| ReleaseID int64 `xorm:"INDEX"` | |||||
| UploaderID int64 `xorm:"INDEX DEFAULT 0"` // Notice: will be zero before this column added | |||||
| CommentID int64 | |||||
| Name string | |||||
| DownloadCount int64 `xorm:"DEFAULT 0"` | |||||
| Size int64 `xorm:"DEFAULT 0"` | |||||
| IsPrivate bool `xorm:"DEFAULT false"` | |||||
| DecompressState int32 `xorm:"DEFAULT 0"` | |||||
| CreatedUnix timeutil.TimeStamp `xorm:"created"` | |||||
| ID int64 `xorm:"pk autoincr"` | |||||
| UUID string `xorm:"uuid UNIQUE"` | |||||
| IssueID int64 `xorm:"INDEX"` | |||||
| DatasetID int64 `xorm:"INDEX DEFAULT 0"` | |||||
| ReleaseID int64 `xorm:"INDEX"` | |||||
| UploaderID int64 `xorm:"INDEX DEFAULT 0"` // Notice: will be zero before this column added | |||||
| CommentID int64 | |||||
| Name string | |||||
| DownloadCount int64 `xorm:"DEFAULT 0"` | |||||
| Size int64 `xorm:"DEFAULT 0"` | |||||
| IsPrivate bool `xorm:"DEFAULT false"` | |||||
| DecompressState int32 `xorm:"DEFAULT 0"` | |||||
| CreatedUnix timeutil.TimeStamp `xorm:"created"` | |||||
| } | } | ||||
| func (a *Attachment) AfterUpdate() { | func (a *Attachment) AfterUpdate() { | ||||
| @@ -1971,3 +1971,19 @@ func IsErrOAuthApplicationNotFound(err error) bool { | |||||
| func (err ErrOAuthApplicationNotFound) Error() string { | func (err ErrOAuthApplicationNotFound) Error() string { | ||||
| return fmt.Sprintf("OAuth application not found [ID: %d]", err.ID) | return fmt.Sprintf("OAuth application not found [ID: %d]", err.ID) | ||||
| } | } | ||||
| // ErrFileChunkNotExist represents a "FileChunkNotExist" kind of error. | |||||
| type ErrFileChunkNotExist struct { | |||||
| Md5 string | |||||
| Uuid string | |||||
| } | |||||
| func (err ErrFileChunkNotExist) Error() string { | |||||
| return fmt.Sprintf("fileChunk does not exist [md5: %s, uuid: %s]", err.Md5, err.Uuid) | |||||
| } | |||||
| // IsErrFileChunkNotExist checks if an error is a ErrFileChunkNotExist. | |||||
| func IsErrFileChunkNotExist(err error) bool { | |||||
| _, ok := err.(ErrFileChunkNotExist) | |||||
| return ok | |||||
| } | |||||
| @@ -0,0 +1,94 @@ | |||||
| package models | |||||
| import ( | |||||
| "code.gitea.io/gitea/modules/timeutil" | |||||
| "xorm.io/xorm" | |||||
| ) | |||||
| const ( | |||||
| FileNotUploaded int = iota | |||||
| FileUploaded | |||||
| ) | |||||
| type FileChunk struct { | |||||
| ID int64 `xorm:"pk autoincr"` | |||||
| UUID string `xorm:"uuid UNIQUE"` | |||||
| Md5 string `xorm:"INDEX"` | |||||
| IsUploaded int `xorm:"DEFAULT 0"` // not uploaded: 0, uploaded: 1 | |||||
| UploadID string `xorm:"UNIQUE"`//minio upload id | |||||
| TotalChunks int | |||||
| Size int64 | |||||
| UserID int64 `xorm:"INDEX"` | |||||
| CompletedParts []string `xorm:"DEFAULT """`// chunkNumber+etag eg: ,1-asqwewqe21312312.2-123hjkas | |||||
| CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` | |||||
| UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` | |||||
| } | |||||
| // GetFileChunkByMD5 returns fileChunk by given id | |||||
| func GetFileChunkByMD5(md5 string) (*FileChunk, error) { | |||||
| return getFileChunkByMD5(x, md5) | |||||
| } | |||||
| func getFileChunkByMD5(e Engine, md5 string) (*FileChunk, error) { | |||||
| fileChunk := new(FileChunk) | |||||
| if has, err := e.Where("md5 = ?", md5).Get(fileChunk); err != nil { | |||||
| return nil, err | |||||
| } else if !has { | |||||
| return nil, ErrFileChunkNotExist{md5, ""} | |||||
| } | |||||
| return fileChunk, nil | |||||
| } | |||||
| // GetFileChunkByMD5 returns fileChunk by given id | |||||
| func GetFileChunkByMD5AndUser(md5 string, userID int64) (*FileChunk, error) { | |||||
| return getFileChunkByMD5AndUser(x, md5, userID) | |||||
| } | |||||
| func getFileChunkByMD5AndUser(e Engine, md5 string, userID int64) (*FileChunk, error) { | |||||
| fileChunk := new(FileChunk) | |||||
| if has, err := e.Where("md5 = ? and user_id = ?", md5, userID).Get(fileChunk); err != nil { | |||||
| return nil, err | |||||
| } else if !has { | |||||
| return nil, ErrFileChunkNotExist{md5, ""} | |||||
| } | |||||
| return fileChunk, nil | |||||
| } | |||||
| // GetAttachmentByID returns attachment by given id | |||||
| func GetFileChunkByUUID(uuid string) (*FileChunk, error) { | |||||
| return getFileChunkByUUID(x, uuid) | |||||
| } | |||||
| func getFileChunkByUUID(e Engine, uuid string) (*FileChunk, error) { | |||||
| fileChunk := new(FileChunk) | |||||
| if has, err := e.Where("uuid = ?", uuid).Get(fileChunk); err != nil { | |||||
| return nil, err | |||||
| } else if !has { | |||||
| return nil, ErrFileChunkNotExist{"", uuid} | |||||
| } | |||||
| return fileChunk, nil | |||||
| } | |||||
| // InsertFileChunk insert a record into file_chunk. | |||||
| func InsertFileChunk(fileChunk *FileChunk) (_ *FileChunk, err error) { | |||||
| if _, err := x.Insert(fileChunk); err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return fileChunk,nil | |||||
| } | |||||
| // UpdateAttachment updates the given attachment in database | |||||
| func UpdateFileChunk(fileChunk *FileChunk) error { | |||||
| return updateFileChunk(x, fileChunk) | |||||
| } | |||||
| func updateFileChunk(e Engine, fileChunk *FileChunk) error { | |||||
| var sess *xorm.Session | |||||
| sess = e.Where("uuid = ?", fileChunk.UUID) | |||||
| _, err := sess.Cols("is_uploaded", "completed_parts").Update(fileChunk) | |||||
| return err | |||||
| } | |||||
| @@ -212,8 +212,6 @@ var migrations = []Migration{ | |||||
| NewMigration("Add ResolveDoerID to Comment table", addResolveDoerIDCommentColumn), | NewMigration("Add ResolveDoerID to Comment table", addResolveDoerIDCommentColumn), | ||||
| // v139 -> v140 | // v139 -> v140 | ||||
| NewMigration("prepend refs/heads/ to issue refs", prependRefsHeadsToIssueRefs), | NewMigration("prepend refs/heads/ to issue refs", prependRefsHeadsToIssueRefs), | ||||
| NewMigration("add dataset migration", addDatasetTable), | |||||
| NewMigration("add cloudbrain migration", addCloudBrainTable), | |||||
| } | } | ||||
| // GetCurrentDBVersion returns the current db version | // GetCurrentDBVersion returns the current db version | ||||
| @@ -1,35 +0,0 @@ | |||||
| // Copyright 2019 The Gitea Authors. All rights reserved. | |||||
| // Use of this source code is governed by a MIT-style | |||||
| // license that can be found in the LICENSE file. | |||||
| package migrations | |||||
| import ( | |||||
| "fmt" | |||||
| "code.gitea.io/gitea/modules/timeutil" | |||||
| "xorm.io/xorm" | |||||
| ) | |||||
| func addDatasetTable(x *xorm.Engine) error { | |||||
| type Dataset struct { | |||||
| ID int64 `xorm:"pk autoincr"` | |||||
| Title string `xorm:"INDEX NOT NULL"` | |||||
| Status int32 `xorm:"INDEX"` | |||||
| Category string | |||||
| Description string `xorm:"TEXT"` | |||||
| DownloadTimes int64 | |||||
| License string | |||||
| Task string | |||||
| ReleaseID int64 `xorm:"INDEX"` | |||||
| UserID int64 `xorm:"INDEX"` | |||||
| RepoID int64 `xorm:"INDEX"` | |||||
| CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` | |||||
| UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` | |||||
| } | |||||
| if err := x.Sync2(new(Dataset)); err != nil { | |||||
| return fmt.Errorf("Sync2: %v", err) | |||||
| } | |||||
| return nil | |||||
| } | |||||
| @@ -1,26 +0,0 @@ | |||||
| package migrations | |||||
| import ( | |||||
| "fmt" | |||||
| "code.gitea.io/gitea/modules/timeutil" | |||||
| "xorm.io/xorm" | |||||
| ) | |||||
| func addCloudBrainTable(x *xorm.Engine) error { | |||||
| type Cloudbrain struct { | |||||
| ID int64 `xorm:"pk autoincr"` | |||||
| JobID string `xorm:"INDEX NOT NULL"` | |||||
| JobName string | |||||
| Status string `xorm:"INDEX"` | |||||
| UserID int64 `xorm:"INDEX"` | |||||
| RepoID int64 `xorm:"INDEX"` | |||||
| CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` | |||||
| UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` | |||||
| } | |||||
| if err := x.Sync2(new(Cloudbrain)); err != nil { | |||||
| return fmt.Errorf("Sync2: %v", err) | |||||
| } | |||||
| return nil | |||||
| } | |||||
| @@ -127,6 +127,7 @@ func init() { | |||||
| new(EmailHash), | new(EmailHash), | ||||
| new(Dataset), | new(Dataset), | ||||
| new(Cloudbrain), | new(Cloudbrain), | ||||
| new(FileChunk), | |||||
| ) | ) | ||||
| gonicNames := []string{"SSL", "UID"} | gonicNames := []string{"SSL", "UID"} | ||||
| @@ -0,0 +1,7 @@ | |||||
| package decompression | |||||
| import "code.gitea.io/gitea/modules/worker" | |||||
| func NewContext() { | |||||
| worker.NewTaskCenter() | |||||
| } | |||||
| @@ -0,0 +1,156 @@ | |||||
| package minio_ext | |||||
| import ( | |||||
| "encoding/xml" | |||||
| "fmt" | |||||
| "net/http" | |||||
| ) | |||||
| type ErrorResponse struct { | |||||
| XMLName xml.Name `xml:"Error" json:"-"` | |||||
| Code string | |||||
| Message string | |||||
| BucketName string | |||||
| Key string | |||||
| RequestID string `xml:"RequestId"` | |||||
| HostID string `xml:"HostId"` | |||||
| // Region where the bucket is located. This header is returned | |||||
| // only in HEAD bucket and ListObjects response. | |||||
| Region string | |||||
| // Underlying HTTP status code for the returned error | |||||
| StatusCode int `xml:"-" json:"-"` | |||||
| } | |||||
| // Error - Returns HTTP error string | |||||
| func (e ErrorResponse) Error() string { | |||||
| return e.Message | |||||
| } | |||||
| const ( | |||||
| reportIssue = "Please report this issue at https://github.com/minio/minio/issues." | |||||
| ) | |||||
| // httpRespToErrorResponse returns a new encoded ErrorResponse | |||||
| // structure as error. | |||||
| func httpRespToErrorResponse(resp *http.Response, bucketName, objectName string) error { | |||||
| if resp == nil { | |||||
| msg := "Response is empty. " + reportIssue | |||||
| return ErrInvalidArgument(msg) | |||||
| } | |||||
| errResp := ErrorResponse{ | |||||
| StatusCode: resp.StatusCode, | |||||
| } | |||||
| err := xmlDecoder(resp.Body, &errResp) | |||||
| // Xml decoding failed with no body, fall back to HTTP headers. | |||||
| if err != nil { | |||||
| switch resp.StatusCode { | |||||
| case http.StatusNotFound: | |||||
| if objectName == "" { | |||||
| errResp = ErrorResponse{ | |||||
| StatusCode: resp.StatusCode, | |||||
| Code: "NoSuchBucket", | |||||
| Message: "The specified bucket does not exist.", | |||||
| BucketName: bucketName, | |||||
| } | |||||
| } else { | |||||
| errResp = ErrorResponse{ | |||||
| StatusCode: resp.StatusCode, | |||||
| Code: "NoSuchKey", | |||||
| Message: "The specified key does not exist.", | |||||
| BucketName: bucketName, | |||||
| Key: objectName, | |||||
| } | |||||
| } | |||||
| case http.StatusForbidden: | |||||
| errResp = ErrorResponse{ | |||||
| StatusCode: resp.StatusCode, | |||||
| Code: "AccessDenied", | |||||
| Message: "Access Denied.", | |||||
| BucketName: bucketName, | |||||
| Key: objectName, | |||||
| } | |||||
| case http.StatusConflict: | |||||
| errResp = ErrorResponse{ | |||||
| StatusCode: resp.StatusCode, | |||||
| Code: "Conflict", | |||||
| Message: "Bucket not empty.", | |||||
| BucketName: bucketName, | |||||
| } | |||||
| case http.StatusPreconditionFailed: | |||||
| errResp = ErrorResponse{ | |||||
| StatusCode: resp.StatusCode, | |||||
| Code: "PreconditionFailed", | |||||
| Message: s3ErrorResponseMap["PreconditionFailed"], | |||||
| BucketName: bucketName, | |||||
| Key: objectName, | |||||
| } | |||||
| default: | |||||
| errResp = ErrorResponse{ | |||||
| StatusCode: resp.StatusCode, | |||||
| Code: resp.Status, | |||||
| Message: resp.Status, | |||||
| BucketName: bucketName, | |||||
| } | |||||
| } | |||||
| } | |||||
| // Save hostID, requestID and region information | |||||
| // from headers if not available through error XML. | |||||
| if errResp.RequestID == "" { | |||||
| errResp.RequestID = resp.Header.Get("x-amz-request-id") | |||||
| } | |||||
| if errResp.HostID == "" { | |||||
| errResp.HostID = resp.Header.Get("x-amz-id-2") | |||||
| } | |||||
| if errResp.Region == "" { | |||||
| errResp.Region = resp.Header.Get("x-amz-bucket-region") | |||||
| } | |||||
| if errResp.Code == "InvalidRegion" && errResp.Region != "" { | |||||
| errResp.Message = fmt.Sprintf("Region does not match, expecting region ‘%s’.", errResp.Region) | |||||
| } | |||||
| return errResp | |||||
| } | |||||
| func ToErrorResponse(err error) ErrorResponse { | |||||
| switch err := err.(type) { | |||||
| case ErrorResponse: | |||||
| return err | |||||
| default: | |||||
| return ErrorResponse{} | |||||
| } | |||||
| } | |||||
| // ErrInvalidArgument - Invalid argument response. | |||||
| func ErrInvalidArgument(message string) error { | |||||
| return ErrorResponse{ | |||||
| Code: "InvalidArgument", | |||||
| Message: message, | |||||
| RequestID: "minio", | |||||
| } | |||||
| } | |||||
| // ErrEntityTooLarge - Input size is larger than supported maximum. | |||||
| func ErrEntityTooLarge(totalSize, maxObjectSize int64, bucketName, objectName string) error { | |||||
| msg := fmt.Sprintf("Your proposed upload size ‘%d’ exceeds the maximum allowed object size ‘%d’ for single PUT operation.", totalSize, maxObjectSize) | |||||
| return ErrorResponse{ | |||||
| StatusCode: http.StatusBadRequest, | |||||
| Code: "EntityTooLarge", | |||||
| Message: msg, | |||||
| BucketName: bucketName, | |||||
| Key: objectName, | |||||
| } | |||||
| } | |||||
| // ErrEntityTooSmall - Input size is smaller than supported minimum. | |||||
| func ErrEntityTooSmall(totalSize int64, bucketName, objectName string) error { | |||||
| msg := fmt.Sprintf("Your proposed upload size ‘%d’ is below the minimum allowed object size ‘0B’ for single PUT operation.", totalSize) | |||||
| return ErrorResponse{ | |||||
| StatusCode: http.StatusBadRequest, | |||||
| Code: "EntityTooSmall", | |||||
| Message: msg, | |||||
| BucketName: bucketName, | |||||
| Key: objectName, | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,62 @@ | |||||
| /* | |||||
| * MinIO Go Library for Amazon S3 Compatible Cloud Storage | |||||
| * Copyright 2015-2017 MinIO, Inc. | |||||
| * | |||||
| * Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| * you may not use this file except in compliance with the License. | |||||
| * You may obtain a copy of the License at | |||||
| * | |||||
| * http://www.apache.org/licenses/LICENSE-2.0 | |||||
| * | |||||
| * Unless required by applicable law or agreed to in writing, software | |||||
| * distributed under the License is distributed on an "AS IS" BASIS, | |||||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| * See the License for the specific language governing permissions and | |||||
| * limitations under the License. | |||||
| */ | |||||
| package minio_ext | |||||
| /// Multipart upload defaults. | |||||
| // absMinPartSize - absolute minimum part size (5 MiB) below which | |||||
| // a part in a multipart upload may not be uploaded. | |||||
| const absMinPartSize = 1024 * 1024 * 5 | |||||
| // minPartSize - minimum part size 128MiB per object after which | |||||
| // putObject behaves internally as multipart. | |||||
| const MinPartSize = 1024 * 1024 * 64 | |||||
| // maxPartsCount - maximum number of parts for a single multipart session. | |||||
| const MaxPartsCount = 10000 | |||||
| // maxPartSize - maximum part size 5GiB for a single multipart upload | |||||
| // operation. | |||||
| const maxPartSize = 1024 * 1024 * 1024 * 5 | |||||
| // maxSinglePutObjectSize - maximum size 5GiB of object per PUT | |||||
| // operation. | |||||
| const maxSinglePutObjectSize = 1024 * 1024 * 1024 * 5 | |||||
| // maxMultipartPutObjectSize - maximum size 5TiB of object for | |||||
| // Multipart operation. | |||||
| const MaxMultipartPutObjectSize = 1024 * 1024 * 1024 * 1024 * 5 | |||||
| // unsignedPayload - value to be set to X-Amz-Content-Sha256 header when | |||||
| // we don't want to sign the request payload | |||||
| const unsignedPayload = "UNSIGNED-PAYLOAD" | |||||
| // Total number of parallel workers used for multipart operation. | |||||
| const totalWorkers = 4 | |||||
| // Signature related constants. | |||||
| const ( | |||||
| signV4Algorithm = "AWS4-HMAC-SHA256" | |||||
| iso8601DateFormat = "20060102T150405Z" | |||||
| ) | |||||
| // Storage class header constant. | |||||
| const amzStorageClass = "X-Amz-Storage-Class" | |||||
| // Website redirect location header constant | |||||
| const amzWebsiteRedirectLocation = "X-Amz-Website-Redirect-Location" | |||||
| @@ -0,0 +1,85 @@ | |||||
| package minio_ext | |||||
| import ( | |||||
| "net/http" | |||||
| "net/url" | |||||
| "time" | |||||
| ) | |||||
| // StringMap represents map with custom UnmarshalXML | |||||
| type StringMap map[string]string | |||||
| // CommonPrefix container for prefix response. | |||||
| type CommonPrefix struct { | |||||
| Prefix string | |||||
| } | |||||
| // ObjectInfo container for object metadata. | |||||
| type ObjectInfo struct { | |||||
| // An ETag is optionally set to md5sum of an object. In case of multipart objects, | |||||
| // ETag is of the form MD5SUM-N where MD5SUM is md5sum of all individual md5sums of | |||||
| // each parts concatenated into one string. | |||||
| ETag string `json:"etag"` | |||||
| Key string `json:"name"` // Name of the object | |||||
| LastModified time.Time `json:"lastModified"` // Date and time the object was last modified. | |||||
| Size int64 `json:"size"` // Size in bytes of the object. | |||||
| ContentType string `json:"contentType"` // A standard MIME type describing the format of the object data. | |||||
| Expires time.Time `json:"expires"` // The date and time at which the object is no longer able to be cached. | |||||
| // Collection of additional metadata on the object. | |||||
| // eg: x-amz-meta-*, content-encoding etc. | |||||
| Metadata http.Header `json:"metadata" xml:"-"` | |||||
| // x-amz-meta-* headers stripped "x-amz-meta-" prefix containing the first value. | |||||
| UserMetadata StringMap `json:"userMetadata"` | |||||
| // Owner name. | |||||
| Owner struct { | |||||
| DisplayName string `json:"name"` | |||||
| ID string `json:"id"` | |||||
| } `json:"owner"` | |||||
| // The class of storage used to store the object. | |||||
| StorageClass string `json:"storageClass"` | |||||
| // Error | |||||
| Err error `json:"-"` | |||||
| } | |||||
| // ListBucketResult container for listObjects response. | |||||
| type ListBucketResult struct { | |||||
| // A response can contain CommonPrefixes only if you have | |||||
| // specified a delimiter. | |||||
| CommonPrefixes []CommonPrefix | |||||
| // Metadata about each object returned. | |||||
| Contents []ObjectInfo | |||||
| Delimiter string | |||||
| // Encoding type used to encode object keys in the response. | |||||
| EncodingType string | |||||
| // A flag that indicates whether or not ListObjects returned all of the results | |||||
| // that satisfied the search criteria. | |||||
| IsTruncated bool | |||||
| Marker string | |||||
| MaxKeys int64 | |||||
| Name string | |||||
| // When response is truncated (the IsTruncated element value in | |||||
| // the response is true), you can use the key name in this field | |||||
| // as marker in the subsequent request to get next set of objects. | |||||
| // Object storage lists objects in alphabetical order Note: This | |||||
| // element is returned only if you have delimiter request | |||||
| // parameter specified. If response does not include the NextMaker | |||||
| // and it is truncated, you can use the value of the last Key in | |||||
| // the response as the marker in the subsequent request to get the | |||||
| // next set of object keys. | |||||
| NextMarker string | |||||
| Prefix string | |||||
| } | |||||
| var ( | |||||
| // Hex encoded string of nil sha256sum bytes. | |||||
| emptySHA256Hex = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" | |||||
| // Sentinel URL is the default url value which is invalid. | |||||
| sentinelURL = url.URL{} | |||||
| ) | |||||
| @@ -0,0 +1,82 @@ | |||||
| // +build go1.7 go1.8 | |||||
| /* | |||||
| * MinIO Go Library for Amazon S3 Compatible Cloud Storage | |||||
| * Copyright 2017-2018 MinIO, Inc. | |||||
| * | |||||
| * Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| * you may not use this file except in compliance with the License. | |||||
| * You may obtain a copy of the License at | |||||
| * | |||||
| * http://www.apache.org/licenses/LICENSE-2.0 | |||||
| * | |||||
| * Unless required by applicable law or agreed to in writing, software | |||||
| * distributed under the License is distributed on an "AS IS" BASIS, | |||||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| * See the License for the specific language governing permissions and | |||||
| * limitations under the License. | |||||
| */ | |||||
| package minio_ext | |||||
| import ( | |||||
| "crypto/tls" | |||||
| "crypto/x509" | |||||
| "net" | |||||
| "net/http" | |||||
| "time" | |||||
| "golang.org/x/net/http2" | |||||
| ) | |||||
| // DefaultTransport - this default transport is similar to | |||||
| // http.DefaultTransport but with additional param DisableCompression | |||||
| // is set to true to avoid decompressing content with 'gzip' encoding. | |||||
| var DefaultTransport = func(secure bool) (http.RoundTripper, error) { | |||||
| tr := &http.Transport{ | |||||
| Proxy: http.ProxyFromEnvironment, | |||||
| DialContext: (&net.Dialer{ | |||||
| Timeout: 30 * time.Second, | |||||
| KeepAlive: 30 * time.Second, | |||||
| }).DialContext, | |||||
| MaxIdleConns: 1024, | |||||
| MaxIdleConnsPerHost: 1024, | |||||
| IdleConnTimeout: 90 * time.Second, | |||||
| TLSHandshakeTimeout: 10 * time.Second, | |||||
| ExpectContinueTimeout: 1 * time.Second, | |||||
| // Set this value so that the underlying transport round-tripper | |||||
| // doesn't try to auto decode the body of objects with | |||||
| // content-encoding set to `gzip`. | |||||
| // | |||||
| // Refer: | |||||
| // https://golang.org/src/net/http/transport.go?h=roundTrip#L1843 | |||||
| DisableCompression: true, | |||||
| } | |||||
| if secure { | |||||
| rootCAs, _ := x509.SystemCertPool() | |||||
| if rootCAs == nil { | |||||
| // In some systems (like Windows) system cert pool is | |||||
| // not supported or no certificates are present on the | |||||
| // system - so we create a new cert pool. | |||||
| rootCAs = x509.NewCertPool() | |||||
| } | |||||
| // Keep TLS config. | |||||
| tlsConfig := &tls.Config{ | |||||
| RootCAs: rootCAs, | |||||
| // Can't use SSLv3 because of POODLE and BEAST | |||||
| // Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher | |||||
| // Can't use TLSv1.1 because of RC4 cipher usage | |||||
| MinVersion: tls.VersionTLS12, | |||||
| } | |||||
| tr.TLSClientConfig = tlsConfig | |||||
| // Because we create a custom TLSClientConfig, we have to opt-in to HTTP/2. | |||||
| // See https://github.com/golang/go/issues/14275 | |||||
| if err := http2.ConfigureTransport(tr); err != nil { | |||||
| return nil, err | |||||
| } | |||||
| } | |||||
| return tr, nil | |||||
| } | |||||
| @@ -0,0 +1,185 @@ | |||||
| package minio_ext | |||||
| import ( | |||||
| "encoding/hex" | |||||
| "encoding/xml" | |||||
| "io" | |||||
| "io/ioutil" | |||||
| "net" | |||||
| "net/http" | |||||
| "net/url" | |||||
| "regexp" | |||||
| "strings" | |||||
| "crypto/sha256" | |||||
| "github.com/minio/minio-go/v6/pkg/s3utils" | |||||
| ) | |||||
| // regCred matches credential string in HTTP header | |||||
| var regCred = regexp.MustCompile("Credential=([A-Z0-9]+)/") | |||||
| // regCred matches signature string in HTTP header | |||||
| var regSign = regexp.MustCompile("Signature=([[0-9a-f]+)") | |||||
| // xmlDecoder provide decoded value in xml. | |||||
| func xmlDecoder(body io.Reader, v interface{}) error { | |||||
| d := xml.NewDecoder(body) | |||||
| return d.Decode(v) | |||||
| } | |||||
| // Redact out signature value from authorization string. | |||||
| func redactSignature(origAuth string) string { | |||||
| if !strings.HasPrefix(origAuth, signV4Algorithm) { | |||||
| // Set a temporary redacted auth | |||||
| return "AWS **REDACTED**:**REDACTED**" | |||||
| } | |||||
| /// Signature V4 authorization header. | |||||
| // Strip out accessKeyID from: | |||||
| // Credential=<access-key-id>/<date>/<aws-region>/<aws-service>/aws4_request | |||||
| newAuth := regCred.ReplaceAllString(origAuth, "Credential=**REDACTED**/") | |||||
| // Strip out 256-bit signature from: Signature=<256-bit signature> | |||||
| return regSign.ReplaceAllString(newAuth, "Signature=**REDACTED**") | |||||
| } | |||||
| // closeResponse close non nil response with any response Body. | |||||
| // convenient wrapper to drain any remaining data on response body. | |||||
| // | |||||
| // Subsequently this allows golang http RoundTripper | |||||
| // to re-use the same connection for future requests. | |||||
| func closeResponse(resp *http.Response) { | |||||
| // Callers should close resp.Body when done reading from it. | |||||
| // If resp.Body is not closed, the Client's underlying RoundTripper | |||||
| // (typically Transport) may not be able to re-use a persistent TCP | |||||
| // connection to the server for a subsequent "keep-alive" request. | |||||
| if resp != nil && resp.Body != nil { | |||||
| // Drain any remaining Body and then close the connection. | |||||
| // Without this closing connection would disallow re-using | |||||
| // the same connection for future uses. | |||||
| // - http://stackoverflow.com/a/17961593/4465767 | |||||
| io.Copy(ioutil.Discard, resp.Body) | |||||
| resp.Body.Close() | |||||
| } | |||||
| } | |||||
| // Verify if input endpoint URL is valid. | |||||
| func isValidEndpointURL(endpointURL url.URL) error { | |||||
| if endpointURL == sentinelURL { | |||||
| return ErrInvalidArgument("Endpoint url cannot be empty.") | |||||
| } | |||||
| if endpointURL.Path != "/" && endpointURL.Path != "" { | |||||
| return ErrInvalidArgument("Endpoint url cannot have fully qualified paths.") | |||||
| } | |||||
| if strings.Contains(endpointURL.Host, ".s3.amazonaws.com") { | |||||
| if !s3utils.IsAmazonEndpoint(endpointURL) { | |||||
| return ErrInvalidArgument("Amazon S3 endpoint should be 's3.amazonaws.com'.") | |||||
| } | |||||
| } | |||||
| if strings.Contains(endpointURL.Host, ".googleapis.com") { | |||||
| if !s3utils.IsGoogleEndpoint(endpointURL) { | |||||
| return ErrInvalidArgument("Google Cloud Storage endpoint should be 'storage.googleapis.com'.") | |||||
| } | |||||
| } | |||||
| return nil | |||||
| } | |||||
| // getEndpointURL - construct a new endpoint. | |||||
| func getEndpointURL(endpoint string, secure bool) (*url.URL, error) { | |||||
| if strings.Contains(endpoint, ":") { | |||||
| host, _, err := net.SplitHostPort(endpoint) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| if !s3utils.IsValidIP(host) && !s3utils.IsValidDomain(host) { | |||||
| msg := "Endpoint: " + endpoint + " does not follow ip address or domain name standards." | |||||
| return nil, ErrInvalidArgument(msg) | |||||
| } | |||||
| } else { | |||||
| if !s3utils.IsValidIP(endpoint) && !s3utils.IsValidDomain(endpoint) { | |||||
| msg := "Endpoint: " + endpoint + " does not follow ip address or domain name standards." | |||||
| return nil, ErrInvalidArgument(msg) | |||||
| } | |||||
| } | |||||
| // If secure is false, use 'http' scheme. | |||||
| scheme := "https" | |||||
| if !secure { | |||||
| scheme = "http" | |||||
| } | |||||
| // Construct a secured endpoint URL. | |||||
| endpointURLStr := scheme + "://" + endpoint | |||||
| endpointURL, err := url.Parse(endpointURLStr) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| // Validate incoming endpoint URL. | |||||
| if err := isValidEndpointURL(*endpointURL); err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return endpointURL, nil | |||||
| } | |||||
| var supportedHeaders = []string{ | |||||
| "content-type", | |||||
| "cache-control", | |||||
| "content-encoding", | |||||
| "content-disposition", | |||||
| "content-language", | |||||
| "x-amz-website-redirect-location", | |||||
| "expires", | |||||
| // Add more supported headers here. | |||||
| } | |||||
| // isStorageClassHeader returns true if the header is a supported storage class header | |||||
| func isStorageClassHeader(headerKey string) bool { | |||||
| return strings.EqualFold(amzStorageClass, headerKey) | |||||
| } | |||||
| // isStandardHeader returns true if header is a supported header and not a custom header | |||||
| func isStandardHeader(headerKey string) bool { | |||||
| key := strings.ToLower(headerKey) | |||||
| for _, header := range supportedHeaders { | |||||
| if strings.ToLower(header) == key { | |||||
| return true | |||||
| } | |||||
| } | |||||
| return false | |||||
| } | |||||
| // sseHeaders is list of server side encryption headers | |||||
| var sseHeaders = []string{ | |||||
| "x-amz-server-side-encryption", | |||||
| "x-amz-server-side-encryption-aws-kms-key-id", | |||||
| "x-amz-server-side-encryption-context", | |||||
| "x-amz-server-side-encryption-customer-algorithm", | |||||
| "x-amz-server-side-encryption-customer-key", | |||||
| "x-amz-server-side-encryption-customer-key-MD5", | |||||
| } | |||||
| // isSSEHeader returns true if header is a server side encryption header. | |||||
| func isSSEHeader(headerKey string) bool { | |||||
| key := strings.ToLower(headerKey) | |||||
| for _, h := range sseHeaders { | |||||
| if strings.ToLower(h) == key { | |||||
| return true | |||||
| } | |||||
| } | |||||
| return false | |||||
| } | |||||
| // isAmzHeader returns true if header is a x-amz-meta-* or x-amz-acl header. | |||||
| func isAmzHeader(headerKey string) bool { | |||||
| key := strings.ToLower(headerKey) | |||||
| return strings.HasPrefix(key, "x-amz-meta-") || strings.HasPrefix(key, "x-amz-grant-") || key == "x-amz-acl" || isSSEHeader(headerKey) | |||||
| } | |||||
| // sum256 calculate sha256sum for an input byte array, returns hex encoded. | |||||
| func sum256Hex(data []byte) string { | |||||
| hash := sha256.New() | |||||
| hash.Write(data) | |||||
| return hex.EncodeToString(hash.Sum(nil)) | |||||
| } | |||||
| @@ -422,9 +422,9 @@ var ( | |||||
| UILocation = time.Local | UILocation = time.Local | ||||
| //Machinery config | //Machinery config | ||||
| Broker string | |||||
| DefaultQueue string | |||||
| ResultBackend string | |||||
| Broker string | |||||
| DefaultQueue string | |||||
| ResultBackend string | |||||
| ) | ) | ||||
| // DateLang transforms standard language locale name to corresponding value in datetime plugin. | // DateLang transforms standard language locale name to corresponding value in datetime plugin. | ||||
| @@ -0,0 +1,159 @@ | |||||
| package storage | |||||
| import ( | |||||
| "encoding/xml" | |||||
| "path" | |||||
| "sort" | |||||
| "strconv" | |||||
| "strings" | |||||
| "sync" | |||||
| "time" | |||||
| "code.gitea.io/gitea/modules/log" | |||||
| "code.gitea.io/gitea/modules/minio_ext" | |||||
| "code.gitea.io/gitea/modules/setting" | |||||
| miniov6 "github.com/minio/minio-go/v6" | |||||
| ) | |||||
| const ( | |||||
| PresignedUploadPartUrlExpireTime = time.Hour * 24 * 7 | |||||
| ) | |||||
| type ComplPart struct { | |||||
| PartNumber int `json:"partNumber"` | |||||
| ETag string `json:"eTag"` | |||||
| } | |||||
| type CompleteParts struct { | |||||
| Data []ComplPart `json:"completedParts"` | |||||
| } | |||||
| // completedParts is a collection of parts sortable by their part numbers. | |||||
| // used for sorting the uploaded parts before completing the multipart request. | |||||
| type completedParts []miniov6.CompletePart | |||||
| func (a completedParts) Len() int { return len(a) } | |||||
| func (a completedParts) Swap(i, j int) { a[i], a[j] = a[j], a[i] } | |||||
| func (a completedParts) Less(i, j int) bool { return a[i].PartNumber < a[j].PartNumber } | |||||
| // completeMultipartUpload container for completing multipart upload. | |||||
| type completeMultipartUpload struct { | |||||
| XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ CompleteMultipartUpload" json:"-"` | |||||
| Parts []miniov6.CompletePart `xml:"Part"` | |||||
| } | |||||
| var ( | |||||
| adminClient * minio_ext.Client = nil | |||||
| coreClient *miniov6.Core = nil | |||||
| ) | |||||
| var mutex *sync.Mutex | |||||
| func init(){ | |||||
| mutex = new(sync.Mutex) | |||||
| } | |||||
| func getClients()(*minio_ext.Client, *miniov6.Core, error){ | |||||
| var client * minio_ext.Client | |||||
| var core *miniov6.Core | |||||
| mutex.Lock() | |||||
| defer mutex.Unlock() | |||||
| if nil != adminClient && nil != coreClient { | |||||
| client = adminClient | |||||
| core = coreClient | |||||
| return client, core, nil | |||||
| } | |||||
| var err error | |||||
| minio := setting.Attachment.Minio | |||||
| if nil == adminClient { | |||||
| adminClient, err = minio_ext.New( | |||||
| minio.Endpoint, | |||||
| minio.AccessKeyID, | |||||
| minio.SecretAccessKey, | |||||
| minio.UseSSL, | |||||
| ) | |||||
| if nil != err{ | |||||
| return nil, nil, err | |||||
| } | |||||
| } | |||||
| client = adminClient | |||||
| if nil == coreClient { | |||||
| coreClient, err = miniov6.NewCore( | |||||
| minio.Endpoint, | |||||
| minio.AccessKeyID, | |||||
| minio.SecretAccessKey, | |||||
| minio.UseSSL, | |||||
| ) | |||||
| if nil != err{ | |||||
| return nil, nil, err | |||||
| } | |||||
| } | |||||
| core = coreClient | |||||
| return client, core, nil | |||||
| } | |||||
| func GenMultiPartSignedUrl(uuid string, uploadId string, partNumber int, partSize int64) (string, error) { | |||||
| minioClient, _, err := getClients() | |||||
| if err != nil { | |||||
| log.Error("getClients failed:", err.Error()) | |||||
| return "", err | |||||
| } | |||||
| minio := setting.Attachment.Minio | |||||
| bucketName := minio.Bucket | |||||
| objectName := strings.TrimPrefix(path.Join(minio.BasePath, path.Join(uuid[0:1], uuid[1:2], uuid)), "/") | |||||
| return minioClient.GenUploadPartSignedUrl(uploadId, bucketName, objectName, partNumber, partSize, PresignedUploadPartUrlExpireTime, setting.Attachment.Minio.Location) | |||||
| } | |||||
| func NewMultiPartUpload(uuid string) (string, error){ | |||||
| _, core, err := getClients() | |||||
| if err != nil { | |||||
| log.Error("getClients failed:", err.Error()) | |||||
| return "", err | |||||
| } | |||||
| minio := setting.Attachment.Minio | |||||
| bucketName := minio.Bucket | |||||
| objectName := strings.TrimPrefix(path.Join(minio.BasePath, path.Join(uuid[0:1], uuid[1:2], uuid)), "/") | |||||
| return core.NewMultipartUpload(bucketName, objectName, miniov6.PutObjectOptions{}) | |||||
| } | |||||
| func CompleteMultiPartUpload(uuid string, uploadID string, complParts []string) (string, error){ | |||||
| _, core, err := getClients() | |||||
| if err != nil { | |||||
| log.Error("getClients failed:", err.Error()) | |||||
| return "", err | |||||
| } | |||||
| minio := setting.Attachment.Minio | |||||
| bucketName := minio.Bucket | |||||
| objectName := strings.TrimPrefix(path.Join(minio.BasePath, path.Join(uuid[0:1], uuid[1:2], uuid)), "/") | |||||
| var complMultipartUpload completeMultipartUpload | |||||
| for _,part := range complParts { | |||||
| partNumber, err := strconv.Atoi(strings.Split(part,"-")[0]) | |||||
| if err != nil { | |||||
| log.Error(err.Error()) | |||||
| return "",err | |||||
| } | |||||
| complMultipartUpload.Parts =append(complMultipartUpload.Parts, miniov6.CompletePart{ | |||||
| PartNumber: partNumber, | |||||
| ETag: strings.Split(part,"-")[1], | |||||
| }) | |||||
| } | |||||
| // Sort all completed parts. | |||||
| sort.Sort(completedParts(complMultipartUpload.Parts)) | |||||
| return core.CompleteMultipartUpload(bucketName, objectName, uploadID, complMultipartUpload.Parts) | |||||
| } | |||||
| @@ -7,15 +7,15 @@ import ( | |||||
| ) | ) | ||||
| const ( | const ( | ||||
| DecompressTimer = time.Minute * 10 | |||||
| DecompressTimer = time.Minute * 10 | |||||
| ) | ) | ||||
| func init() { | func init() { | ||||
| ticker := time.NewTicker(DecompressTimer) | ticker := time.NewTicker(DecompressTimer) | ||||
| go func() { | go func() { | ||||
| for { | for { | ||||
| <- ticker.C | |||||
| <-ticker.C | |||||
| repo.HandleUnDecompressAttachment() | repo.HandleUnDecompressAttachment() | ||||
| } | } | ||||
| } () | |||||
| }() | |||||
| } | } | ||||
| @@ -10,10 +10,10 @@ import ( | |||||
| // 方法名 | // 方法名 | ||||
| const ( | const ( | ||||
| DecompressTaskName = "Decompress" | |||||
| DecompressTaskName = "Decompress" | |||||
| ) | ) | ||||
| func SendDecompressTask(ctx context.Context, uuid string) error{ | |||||
| func SendDecompressTask(ctx context.Context, uuid string) error { | |||||
| args := []tasks.Arg{{Name: "uuid", Type: "string", Value: uuid}} | args := []tasks.Arg{{Name: "uuid", Type: "string", Value: uuid}} | ||||
| task, err := tasks.NewSignature(DecompressTaskName, args) | task, err := tasks.NewSignature(DecompressTaskName, args) | ||||
| if err != nil { | if err != nil { | ||||
| @@ -22,7 +22,7 @@ func SendDecompressTask(ctx context.Context, uuid string) error{ | |||||
| } | } | ||||
| task.RetryCount = 3 | task.RetryCount = 3 | ||||
| _,err = AsyncTaskCenter.SendTaskWithContext(ctx, task) | |||||
| _, err = AsyncTaskCenter.SendTaskWithContext(ctx, task) | |||||
| if err != nil { | if err != nil { | ||||
| log.Error("SendTaskWithContext failed:", err.Error()) | log.Error("SendTaskWithContext failed:", err.Error()) | ||||
| return err | return err | ||||
| @@ -1,6 +1,7 @@ | |||||
| package worker | package worker | ||||
| import ( | import ( | ||||
| "code.gitea.io/gitea/modules/setting" | |||||
| "github.com/RichardKnop/machinery/v1" | "github.com/RichardKnop/machinery/v1" | ||||
| mchConf "github.com/RichardKnop/machinery/v1/config" | mchConf "github.com/RichardKnop/machinery/v1/config" | ||||
| ) | ) | ||||
| @@ -9,21 +10,15 @@ var ( | |||||
| AsyncTaskCenter *machinery.Server | AsyncTaskCenter *machinery.Server | ||||
| ) | ) | ||||
| func init() { | |||||
| tc, err := NewTaskCenter() | |||||
| func NewTaskCenter() { | |||||
| cnf := &mchConf.Config{ | |||||
| Broker: setting.Broker, | |||||
| DefaultQueue: setting.DefaultQueue, | |||||
| ResultBackend: setting.ResultBackend, | |||||
| } | |||||
| tc, err := machinery.NewServer(cnf) | |||||
| if err != nil { | if err != nil { | ||||
| panic(err) | panic(err) | ||||
| } | } | ||||
| AsyncTaskCenter = tc | AsyncTaskCenter = tc | ||||
| } | } | ||||
| func NewTaskCenter() (*machinery.Server, error) { | |||||
| cnf := &mchConf.Config{ | |||||
| Broker: "redis://localhost:6379", | |||||
| DefaultQueue: "DecompressTasksQueue", | |||||
| ResultBackend: "redis://localhost:6379", | |||||
| } | |||||
| // Create server instance | |||||
| return machinery.NewServer(cnf) | |||||
| } | |||||
| @@ -59,6 +59,7 @@ manage_org = Manage Organizations | |||||
| admin_panel = Site Administration | admin_panel = Site Administration | ||||
| account_settings = Account Settings | account_settings = Account Settings | ||||
| settings = Settings | settings = Settings | ||||
| your_dashboard = Dashboard | |||||
| your_profile = Profile | your_profile = Profile | ||||
| your_starred = Starred | your_starred = Starred | ||||
| your_settings = Settings | your_settings = Settings | ||||
| @@ -2426,6 +2427,14 @@ default_message = Drop files or click here to upload. | |||||
| invalid_input_type = You can not upload files of this type. | invalid_input_type = You can not upload files of this type. | ||||
| file_too_big = File size ({{filesize}} MB) exceeds the maximum size of ({{maxFilesize}} MB). | file_too_big = File size ({{filesize}} MB) exceeds the maximum size of ({{maxFilesize}} MB). | ||||
| remove_file = Remove file | remove_file = Remove file | ||||
| file_status = Upload status: | |||||
| file_init_status = Drop files or click here to upload. | |||||
| waitting_uploading = Please wait for the first file transfer to complete | |||||
| md5_computing = MD5 computing | |||||
| loading_file = Loading | |||||
| uploading = Uploading | |||||
| upload_complete = Uploading complete | |||||
| enable_minio_support = Enable minio support to use the dataset service | |||||
| [notification] | [notification] | ||||
| notifications = Notifications | notifications = Notifications | ||||
| @@ -59,6 +59,7 @@ manage_org=管理我的组织 | |||||
| admin_panel=管理后台 | admin_panel=管理后台 | ||||
| account_settings=帐户设置 | account_settings=帐户设置 | ||||
| settings=帐户设置 | settings=帐户设置 | ||||
| your_dashboard=个人概览 | |||||
| your_profile=个人信息 | your_profile=个人信息 | ||||
| your_starred=已点赞 | your_starred=已点赞 | ||||
| your_settings=设置 | your_settings=设置 | ||||
| @@ -2428,6 +2429,14 @@ default_message=拖动文件或者点击此处上传。 | |||||
| invalid_input_type=您不能上传该类型的文件 | invalid_input_type=您不能上传该类型的文件 | ||||
| file_too_big=文件体积({{filesize}} MB)超过了最大允许体积({{maxFilesize}} MB) | file_too_big=文件体积({{filesize}} MB)超过了最大允许体积({{maxFilesize}} MB) | ||||
| remove_file=移除文件 | remove_file=移除文件 | ||||
| file_status=文件处理状态: | |||||
| file_init_status=等待上传 | |||||
| waitting_uploading=请等待文件传输完成 | |||||
| md5_computing=计算MD5 | |||||
| loading_file=加载文件 | |||||
| uploading=正在上传 | |||||
| upload_complete=上传完成 | |||||
| enable_minio_support=启用minio支持以使用数据集服务 | |||||
| [notification] | [notification] | ||||
| notifications=通知 | notifications=通知 | ||||
| @@ -1336,8 +1336,8 @@ | |||||
| }, | }, | ||||
| "acorn-jsx": { | "acorn-jsx": { | ||||
| "version": "5.2.0", | "version": "5.2.0", | ||||
| "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", | |||||
| "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", | |||||
| "resolved": "https://registry.npm.taobao.org/acorn-jsx/download/acorn-jsx-5.2.0.tgz?cache=0&sync_timestamp=1589684116279&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Facorn-jsx%2Fdownload%2Facorn-jsx-5.2.0.tgz", | |||||
| "integrity": "sha1-TGYGkXPW/daO2FI5/CViJhgrLr4=", | |||||
| "dev": true | "dev": true | ||||
| }, | }, | ||||
| "aggregate-error": { | "aggregate-error": { | ||||
| @@ -1919,6 +1919,14 @@ | |||||
| "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==", | "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==", | ||||
| "optional": true | "optional": true | ||||
| }, | }, | ||||
| "axios": { | |||||
| "version": "0.19.2", | |||||
| "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", | |||||
| "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", | |||||
| "requires": { | |||||
| "follow-redirects": "1.5.10" | |||||
| } | |||||
| }, | |||||
| "babel-loader": { | "babel-loader": { | ||||
| "version": "8.1.0", | "version": "8.1.0", | ||||
| "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.1.0.tgz", | "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.1.0.tgz", | ||||
| @@ -3657,8 +3665,8 @@ | |||||
| }, | }, | ||||
| "doctrine": { | "doctrine": { | ||||
| "version": "3.0.0", | "version": "3.0.0", | ||||
| "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", | |||||
| "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", | |||||
| "resolved": "https://registry.npm.taobao.org/doctrine/download/doctrine-3.0.0.tgz", | |||||
| "integrity": "sha1-rd6+rXKmV023g2OdyHoSF3OXOWE=", | |||||
| "dev": true, | "dev": true, | ||||
| "requires": { | "requires": { | ||||
| "esutils": "^2.0.2" | "esutils": "^2.0.2" | ||||
| @@ -3739,9 +3747,9 @@ | |||||
| } | } | ||||
| }, | }, | ||||
| "dropzone": { | "dropzone": { | ||||
| "version": "5.7.0", | |||||
| "resolved": "https://registry.npmjs.org/dropzone/-/dropzone-5.7.0.tgz", | |||||
| "integrity": "sha512-kOltiZXH5cO/72I22JjE+w6BoT6uaVLfWdFMsi1PMKFkU6BZWpqRwjnsRm0o6ANGTBuZar5Piu7m/CbKqRPiYg==" | |||||
| "version": "5.7.2", | |||||
| "resolved": "https://registry.npm.taobao.org/dropzone/download/dropzone-5.7.2.tgz?cache=0&sync_timestamp=1596009792692&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdropzone%2Fdownload%2Fdropzone-5.7.2.tgz", | |||||
| "integrity": "sha1-kb7hVy3aUV1AkB2jBLx53d8wm0w=" | |||||
| }, | }, | ||||
| "duplexer2": { | "duplexer2": { | ||||
| "version": "0.0.2", | "version": "0.0.2", | ||||
| @@ -4014,8 +4022,8 @@ | |||||
| }, | }, | ||||
| "eslint": { | "eslint": { | ||||
| "version": "6.8.0", | "version": "6.8.0", | ||||
| "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", | |||||
| "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", | |||||
| "resolved": "https://registry.npm.taobao.org/eslint/download/eslint-6.8.0.tgz", | |||||
| "integrity": "sha1-YiYtZylzn5J1cjgkMC+yJ8jJP/s=", | |||||
| "dev": true, | "dev": true, | ||||
| "requires": { | "requires": { | ||||
| "@babel/code-frame": "^7.0.0", | "@babel/code-frame": "^7.0.0", | ||||
| @@ -4059,8 +4067,8 @@ | |||||
| "dependencies": { | "dependencies": { | ||||
| "ansi-escapes": { | "ansi-escapes": { | ||||
| "version": "4.3.1", | "version": "4.3.1", | ||||
| "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", | |||||
| "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", | |||||
| "resolved": "https://registry.npm.taobao.org/ansi-escapes/download/ansi-escapes-4.3.1.tgz", | |||||
| "integrity": "sha1-pcR8xDGB8fOP/XB2g3cA05VSKmE=", | |||||
| "dev": true, | "dev": true, | ||||
| "requires": { | "requires": { | ||||
| "type-fest": "^0.11.0" | "type-fest": "^0.11.0" | ||||
| @@ -4068,22 +4076,22 @@ | |||||
| "dependencies": { | "dependencies": { | ||||
| "type-fest": { | "type-fest": { | ||||
| "version": "0.11.0", | "version": "0.11.0", | ||||
| "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", | |||||
| "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", | |||||
| "resolved": "https://registry.npm.taobao.org/type-fest/download/type-fest-0.11.0.tgz", | |||||
| "integrity": "sha1-l6vwhyMQ/tiKXEZrJWgVdhReM/E=", | |||||
| "dev": true | "dev": true | ||||
| } | } | ||||
| } | } | ||||
| }, | }, | ||||
| "ansi-regex": { | "ansi-regex": { | ||||
| "version": "5.0.0", | "version": "5.0.0", | ||||
| "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", | |||||
| "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", | |||||
| "resolved": "https://registry.npm.taobao.org/ansi-regex/download/ansi-regex-5.0.0.tgz", | |||||
| "integrity": "sha1-OIU59VF5vzkznIGvMKZU1p+Hy3U=", | |||||
| "dev": true | "dev": true | ||||
| }, | }, | ||||
| "ansi-styles": { | "ansi-styles": { | ||||
| "version": "4.2.1", | "version": "4.2.1", | ||||
| "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", | |||||
| "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", | |||||
| "resolved": "https://registry.npm.taobao.org/ansi-styles/download/ansi-styles-4.2.1.tgz", | |||||
| "integrity": "sha1-kK51xCTQCNJiTFvynq0xd+v881k=", | |||||
| "dev": true, | "dev": true, | ||||
| "requires": { | "requires": { | ||||
| "@types/color-name": "^1.1.1", | "@types/color-name": "^1.1.1", | ||||
| @@ -4092,17 +4100,23 @@ | |||||
| }, | }, | ||||
| "cli-cursor": { | "cli-cursor": { | ||||
| "version": "3.1.0", | "version": "3.1.0", | ||||
| "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", | |||||
| "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", | |||||
| "resolved": "https://registry.npm.taobao.org/cli-cursor/download/cli-cursor-3.1.0.tgz", | |||||
| "integrity": "sha1-JkMFp65JDR0Dvwybp8kl0XU68wc=", | |||||
| "dev": true, | "dev": true, | ||||
| "requires": { | "requires": { | ||||
| "restore-cursor": "^3.1.0" | "restore-cursor": "^3.1.0" | ||||
| } | } | ||||
| }, | }, | ||||
| "cli-width": { | |||||
| "version": "3.0.0", | |||||
| "resolved": "https://registry.npm.taobao.org/cli-width/download/cli-width-3.0.0.tgz", | |||||
| "integrity": "sha1-ovSEN6LKqaIkNueUvwceyeYc7fY=", | |||||
| "dev": true | |||||
| }, | |||||
| "color-convert": { | "color-convert": { | ||||
| "version": "2.0.1", | "version": "2.0.1", | ||||
| "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", | |||||
| "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", | |||||
| "resolved": "https://registry.npm.taobao.org/color-convert/download/color-convert-2.0.1.tgz", | |||||
| "integrity": "sha1-ctOmjVmMm9s68q0ehPIdiWq9TeM=", | |||||
| "dev": true, | "dev": true, | ||||
| "requires": { | "requires": { | ||||
| "color-name": "~1.1.4" | "color-name": "~1.1.4" | ||||
| @@ -4110,20 +4124,20 @@ | |||||
| }, | }, | ||||
| "color-name": { | "color-name": { | ||||
| "version": "1.1.4", | "version": "1.1.4", | ||||
| "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", | |||||
| "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", | |||||
| "resolved": "https://registry.npm.taobao.org/color-name/download/color-name-1.1.4.tgz", | |||||
| "integrity": "sha1-wqCah6y95pVD3m9j+jmVyCbFNqI=", | |||||
| "dev": true | "dev": true | ||||
| }, | }, | ||||
| "emoji-regex": { | "emoji-regex": { | ||||
| "version": "8.0.0", | "version": "8.0.0", | ||||
| "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", | |||||
| "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", | |||||
| "resolved": "https://registry.npm.taobao.org/emoji-regex/download/emoji-regex-8.0.0.tgz", | |||||
| "integrity": "sha1-6Bj9ac5cz8tARZT4QpY79TFkzDc=", | |||||
| "dev": true | "dev": true | ||||
| }, | }, | ||||
| "eslint-scope": { | "eslint-scope": { | ||||
| "version": "5.0.0", | |||||
| "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", | |||||
| "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", | |||||
| "version": "5.1.0", | |||||
| "resolved": "https://registry.npm.taobao.org/eslint-scope/download/eslint-scope-5.1.0.tgz?cache=0&sync_timestamp=1591269986906&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Feslint-scope%2Fdownload%2Feslint-scope-5.1.0.tgz", | |||||
| "integrity": "sha1-0Plx3+WcaeDK2mhLI9Sdv4JgDOU=", | |||||
| "dev": true, | "dev": true, | ||||
| "requires": { | "requires": { | ||||
| "esrecurse": "^4.1.0", | "esrecurse": "^4.1.0", | ||||
| @@ -4132,8 +4146,8 @@ | |||||
| }, | }, | ||||
| "figures": { | "figures": { | ||||
| "version": "3.2.0", | "version": "3.2.0", | ||||
| "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", | |||||
| "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", | |||||
| "resolved": "https://registry.npm.taobao.org/figures/download/figures-3.2.0.tgz", | |||||
| "integrity": "sha1-YlwYvSk8YE3EqN2y/r8MiDQXRq8=", | |||||
| "dev": true, | "dev": true, | ||||
| "requires": { | "requires": { | ||||
| "escape-string-regexp": "^1.0.5" | "escape-string-regexp": "^1.0.5" | ||||
| @@ -4141,8 +4155,8 @@ | |||||
| }, | }, | ||||
| "globals": { | "globals": { | ||||
| "version": "12.4.0", | "version": "12.4.0", | ||||
| "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", | |||||
| "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", | |||||
| "resolved": "https://registry.npm.taobao.org/globals/download/globals-12.4.0.tgz", | |||||
| "integrity": "sha1-oYgTV2pBsAokqX5/gVkYwuGZJfg=", | |||||
| "dev": true, | "dev": true, | ||||
| "requires": { | "requires": { | ||||
| "type-fest": "^0.8.1" | "type-fest": "^0.8.1" | ||||
| @@ -4150,14 +4164,14 @@ | |||||
| }, | }, | ||||
| "has-flag": { | "has-flag": { | ||||
| "version": "4.0.0", | "version": "4.0.0", | ||||
| "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", | |||||
| "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", | |||||
| "resolved": "https://registry.npm.taobao.org/has-flag/download/has-flag-4.0.0.tgz", | |||||
| "integrity": "sha1-lEdx/ZyByBJlxNaUGGDaBrtZR5s=", | |||||
| "dev": true | "dev": true | ||||
| }, | }, | ||||
| "import-fresh": { | "import-fresh": { | ||||
| "version": "3.2.1", | "version": "3.2.1", | ||||
| "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", | |||||
| "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", | |||||
| "resolved": "https://registry.npm.taobao.org/import-fresh/download/import-fresh-3.2.1.tgz?cache=0&sync_timestamp=1589682760620&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fimport-fresh%2Fdownload%2Fimport-fresh-3.2.1.tgz", | |||||
| "integrity": "sha1-Yz/2GFBueTr1rJG/SLcmd+FcvmY=", | |||||
| "dev": true, | "dev": true, | ||||
| "requires": { | "requires": { | ||||
| "parent-module": "^1.0.0", | "parent-module": "^1.0.0", | ||||
| @@ -4165,40 +4179,46 @@ | |||||
| } | } | ||||
| }, | }, | ||||
| "inquirer": { | "inquirer": { | ||||
| "version": "7.1.0", | |||||
| "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz", | |||||
| "integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==", | |||||
| "version": "7.3.3", | |||||
| "resolved": "https://registry.npm.taobao.org/inquirer/download/inquirer-7.3.3.tgz", | |||||
| "integrity": "sha1-BNF2sq8Er8FXqD/XwQDpjuCq0AM=", | |||||
| "dev": true, | "dev": true, | ||||
| "requires": { | "requires": { | ||||
| "ansi-escapes": "^4.2.1", | "ansi-escapes": "^4.2.1", | ||||
| "chalk": "^3.0.0", | |||||
| "chalk": "^4.1.0", | |||||
| "cli-cursor": "^3.1.0", | "cli-cursor": "^3.1.0", | ||||
| "cli-width": "^2.0.0", | |||||
| "cli-width": "^3.0.0", | |||||
| "external-editor": "^3.0.3", | "external-editor": "^3.0.3", | ||||
| "figures": "^3.0.0", | "figures": "^3.0.0", | ||||
| "lodash": "^4.17.15", | |||||
| "lodash": "^4.17.19", | |||||
| "mute-stream": "0.0.8", | "mute-stream": "0.0.8", | ||||
| "run-async": "^2.4.0", | "run-async": "^2.4.0", | ||||
| "rxjs": "^6.5.3", | |||||
| "rxjs": "^6.6.0", | |||||
| "string-width": "^4.1.0", | "string-width": "^4.1.0", | ||||
| "strip-ansi": "^6.0.0", | "strip-ansi": "^6.0.0", | ||||
| "through": "^2.3.6" | "through": "^2.3.6" | ||||
| }, | }, | ||||
| "dependencies": { | "dependencies": { | ||||
| "chalk": { | "chalk": { | ||||
| "version": "3.0.0", | |||||
| "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", | |||||
| "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", | |||||
| "version": "4.1.0", | |||||
| "resolved": "https://registry.npm.taobao.org/chalk/download/chalk-4.1.0.tgz", | |||||
| "integrity": "sha1-ThSHCmGNni7dl92DRf2dncMVZGo=", | |||||
| "dev": true, | "dev": true, | ||||
| "requires": { | "requires": { | ||||
| "ansi-styles": "^4.1.0", | "ansi-styles": "^4.1.0", | ||||
| "supports-color": "^7.1.0" | "supports-color": "^7.1.0" | ||||
| } | } | ||||
| }, | }, | ||||
| "lodash": { | |||||
| "version": "4.17.20", | |||||
| "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", | |||||
| "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=", | |||||
| "dev": true | |||||
| }, | |||||
| "strip-ansi": { | "strip-ansi": { | ||||
| "version": "6.0.0", | "version": "6.0.0", | ||||
| "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", | |||||
| "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", | |||||
| "resolved": "https://registry.npm.taobao.org/strip-ansi/download/strip-ansi-6.0.0.tgz?cache=0&sync_timestamp=1589682795383&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fstrip-ansi%2Fdownload%2Fstrip-ansi-6.0.0.tgz", | |||||
| "integrity": "sha1-CxVx3XZpzNTz4G4U7x7tJiJa5TI=", | |||||
| "dev": true, | "dev": true, | ||||
| "requires": { | "requires": { | ||||
| "ansi-regex": "^5.0.0" | "ansi-regex": "^5.0.0" | ||||
| @@ -4208,26 +4228,26 @@ | |||||
| }, | }, | ||||
| "is-fullwidth-code-point": { | "is-fullwidth-code-point": { | ||||
| "version": "3.0.0", | "version": "3.0.0", | ||||
| "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", | |||||
| "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", | |||||
| "resolved": "https://registry.npm.taobao.org/is-fullwidth-code-point/download/is-fullwidth-code-point-3.0.0.tgz", | |||||
| "integrity": "sha1-8Rb4Bk/pCz94RKOJl8C3UFEmnx0=", | |||||
| "dev": true | "dev": true | ||||
| }, | }, | ||||
| "mimic-fn": { | "mimic-fn": { | ||||
| "version": "2.1.0", | "version": "2.1.0", | ||||
| "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", | |||||
| "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", | |||||
| "resolved": "https://registry.npm.taobao.org/mimic-fn/download/mimic-fn-2.1.0.tgz?cache=0&sync_timestamp=1596095644798&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fmimic-fn%2Fdownload%2Fmimic-fn-2.1.0.tgz", | |||||
| "integrity": "sha1-ftLCzMyvhNP/y3pptXcR/CCDQBs=", | |||||
| "dev": true | "dev": true | ||||
| }, | }, | ||||
| "mute-stream": { | "mute-stream": { | ||||
| "version": "0.0.8", | "version": "0.0.8", | ||||
| "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", | |||||
| "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", | |||||
| "resolved": "https://registry.npm.taobao.org/mute-stream/download/mute-stream-0.0.8.tgz", | |||||
| "integrity": "sha1-FjDEKyJR/4HiooPelqVJfqkuXg0=", | |||||
| "dev": true | "dev": true | ||||
| }, | }, | ||||
| "onetime": { | "onetime": { | ||||
| "version": "5.1.0", | |||||
| "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", | |||||
| "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", | |||||
| "version": "5.1.2", | |||||
| "resolved": "https://registry.npm.taobao.org/onetime/download/onetime-5.1.2.tgz?cache=0&sync_timestamp=1597005345612&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fonetime%2Fdownload%2Fonetime-5.1.2.tgz", | |||||
| "integrity": "sha1-0Oluu1awdHbfHdnEgG5SN5hcpF4=", | |||||
| "dev": true, | "dev": true, | ||||
| "requires": { | "requires": { | ||||
| "mimic-fn": "^2.1.0" | "mimic-fn": "^2.1.0" | ||||
| @@ -4235,30 +4255,39 @@ | |||||
| }, | }, | ||||
| "resolve-from": { | "resolve-from": { | ||||
| "version": "4.0.0", | "version": "4.0.0", | ||||
| "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", | |||||
| "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", | |||||
| "resolved": "https://registry.npm.taobao.org/resolve-from/download/resolve-from-4.0.0.tgz", | |||||
| "integrity": "sha1-SrzYUq0y3Xuqv+m0DgCjbbXzkuY=", | |||||
| "dev": true | "dev": true | ||||
| }, | }, | ||||
| "restore-cursor": { | "restore-cursor": { | ||||
| "version": "3.1.0", | "version": "3.1.0", | ||||
| "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", | |||||
| "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", | |||||
| "resolved": "https://registry.npm.taobao.org/restore-cursor/download/restore-cursor-3.1.0.tgz", | |||||
| "integrity": "sha1-OfZ8VLOnpYzqUjbZXPADQjljH34=", | |||||
| "dev": true, | "dev": true, | ||||
| "requires": { | "requires": { | ||||
| "onetime": "^5.1.0", | "onetime": "^5.1.0", | ||||
| "signal-exit": "^3.0.2" | "signal-exit": "^3.0.2" | ||||
| } | } | ||||
| }, | }, | ||||
| "rxjs": { | |||||
| "version": "6.6.2", | |||||
| "resolved": "https://registry.npm.taobao.org/rxjs/download/rxjs-6.6.2.tgz", | |||||
| "integrity": "sha1-gJanrAPyzE/lhg725XKBDZ4BwNI=", | |||||
| "dev": true, | |||||
| "requires": { | |||||
| "tslib": "^1.9.0" | |||||
| } | |||||
| }, | |||||
| "semver": { | "semver": { | ||||
| "version": "6.3.0", | "version": "6.3.0", | ||||
| "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", | |||||
| "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", | |||||
| "resolved": "https://registry.npm.taobao.org/semver/download/semver-6.3.0.tgz?cache=0&sync_timestamp=1589682805026&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsemver%2Fdownload%2Fsemver-6.3.0.tgz", | |||||
| "integrity": "sha1-7gpkyK9ejO6mdoexM3YeG+y9HT0=", | |||||
| "dev": true | "dev": true | ||||
| }, | }, | ||||
| "string-width": { | "string-width": { | ||||
| "version": "4.2.0", | "version": "4.2.0", | ||||
| "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", | |||||
| "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", | |||||
| "resolved": "https://registry.npm.taobao.org/string-width/download/string-width-4.2.0.tgz", | |||||
| "integrity": "sha1-lSGCxGzHssMT0VluYjmSvRY7crU=", | |||||
| "dev": true, | "dev": true, | ||||
| "requires": { | "requires": { | ||||
| "emoji-regex": "^8.0.0", | "emoji-regex": "^8.0.0", | ||||
| @@ -4268,8 +4297,8 @@ | |||||
| "dependencies": { | "dependencies": { | ||||
| "strip-ansi": { | "strip-ansi": { | ||||
| "version": "6.0.0", | "version": "6.0.0", | ||||
| "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", | |||||
| "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", | |||||
| "resolved": "https://registry.npm.taobao.org/strip-ansi/download/strip-ansi-6.0.0.tgz?cache=0&sync_timestamp=1589682795383&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fstrip-ansi%2Fdownload%2Fstrip-ansi-6.0.0.tgz", | |||||
| "integrity": "sha1-CxVx3XZpzNTz4G4U7x7tJiJa5TI=", | |||||
| "dev": true, | "dev": true, | ||||
| "requires": { | "requires": { | ||||
| "ansi-regex": "^5.0.0" | "ansi-regex": "^5.0.0" | ||||
| @@ -4279,8 +4308,8 @@ | |||||
| }, | }, | ||||
| "strip-ansi": { | "strip-ansi": { | ||||
| "version": "5.2.0", | "version": "5.2.0", | ||||
| "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", | |||||
| "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", | |||||
| "resolved": "https://registry.npm.taobao.org/strip-ansi/download/strip-ansi-5.2.0.tgz?cache=0&sync_timestamp=1589682795383&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fstrip-ansi%2Fdownload%2Fstrip-ansi-5.2.0.tgz", | |||||
| "integrity": "sha1-jJpTb+tq/JYr36WxBKUJHBrZwK4=", | |||||
| "dev": true, | "dev": true, | ||||
| "requires": { | "requires": { | ||||
| "ansi-regex": "^4.1.0" | "ansi-regex": "^4.1.0" | ||||
| @@ -4288,22 +4317,22 @@ | |||||
| "dependencies": { | "dependencies": { | ||||
| "ansi-regex": { | "ansi-regex": { | ||||
| "version": "4.1.0", | "version": "4.1.0", | ||||
| "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", | |||||
| "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", | |||||
| "resolved": "https://registry.npm.taobao.org/ansi-regex/download/ansi-regex-4.1.0.tgz", | |||||
| "integrity": "sha1-i5+PCM8ay4Q3Vqg5yox+MWjFGZc=", | |||||
| "dev": true | "dev": true | ||||
| } | } | ||||
| } | } | ||||
| }, | }, | ||||
| "strip-json-comments": { | "strip-json-comments": { | ||||
| "version": "3.1.0", | |||||
| "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.0.tgz", | |||||
| "integrity": "sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w==", | |||||
| "version": "3.1.1", | |||||
| "resolved": "https://registry.npm.taobao.org/strip-json-comments/download/strip-json-comments-3.1.1.tgz?cache=0&sync_timestamp=1594567532500&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fstrip-json-comments%2Fdownload%2Fstrip-json-comments-3.1.1.tgz", | |||||
| "integrity": "sha1-MfEoGzgyYwQ0gxwxDAHMzajL4AY=", | |||||
| "dev": true | "dev": true | ||||
| }, | }, | ||||
| "supports-color": { | "supports-color": { | ||||
| "version": "7.1.0", | "version": "7.1.0", | ||||
| "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", | |||||
| "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", | |||||
| "resolved": "https://registry.npm.taobao.org/supports-color/download/supports-color-7.1.0.tgz", | |||||
| "integrity": "sha1-aOMlkd9z4lrRxLSRCKLsUHliv9E=", | |||||
| "dev": true, | "dev": true, | ||||
| "requires": { | "requires": { | ||||
| "has-flag": "^4.0.0" | "has-flag": "^4.0.0" | ||||
| @@ -4495,6 +4524,17 @@ | |||||
| } | } | ||||
| } | } | ||||
| }, | }, | ||||
| "eslint-plugin-vue": { | |||||
| "version": "6.2.2", | |||||
| "resolved": "https://registry.npm.taobao.org/eslint-plugin-vue/download/eslint-plugin-vue-6.2.2.tgz?cache=0&sync_timestamp=1597198168566&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Feslint-plugin-vue%2Fdownload%2Feslint-plugin-vue-6.2.2.tgz", | |||||
| "integrity": "sha1-J/7NmjokeJsPER7N1UCp5WGY4P4=", | |||||
| "dev": true, | |||||
| "requires": { | |||||
| "natural-compare": "^1.4.0", | |||||
| "semver": "^5.6.0", | |||||
| "vue-eslint-parser": "^7.0.0" | |||||
| } | |||||
| }, | |||||
| "eslint-scope": { | "eslint-scope": { | ||||
| "version": "4.0.3", | "version": "4.0.3", | ||||
| "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", | "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", | ||||
| @@ -4506,23 +4546,23 @@ | |||||
| }, | }, | ||||
| "eslint-utils": { | "eslint-utils": { | ||||
| "version": "1.4.3", | "version": "1.4.3", | ||||
| "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", | |||||
| "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", | |||||
| "resolved": "https://registry.npm.taobao.org/eslint-utils/download/eslint-utils-1.4.3.tgz", | |||||
| "integrity": "sha1-dP7HxU0Hdrb2fgJRBAtYBlZOmB8=", | |||||
| "dev": true, | "dev": true, | ||||
| "requires": { | "requires": { | ||||
| "eslint-visitor-keys": "^1.1.0" | "eslint-visitor-keys": "^1.1.0" | ||||
| } | } | ||||
| }, | }, | ||||
| "eslint-visitor-keys": { | "eslint-visitor-keys": { | ||||
| "version": "1.1.0", | |||||
| "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", | |||||
| "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", | |||||
| "version": "1.3.0", | |||||
| "resolved": "https://registry.npm.taobao.org/eslint-visitor-keys/download/eslint-visitor-keys-1.3.0.tgz?cache=0&sync_timestamp=1597435068105&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Feslint-visitor-keys%2Fdownload%2Feslint-visitor-keys-1.3.0.tgz", | |||||
| "integrity": "sha1-MOvR73wv3/AcOk8VEESvJfqwUj4=", | |||||
| "dev": true | "dev": true | ||||
| }, | }, | ||||
| "espree": { | "espree": { | ||||
| "version": "6.2.1", | "version": "6.2.1", | ||||
| "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", | |||||
| "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", | |||||
| "resolved": "https://registry.npm.taobao.org/espree/download/espree-6.2.1.tgz?cache=0&sync_timestamp=1595034145062&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fespree%2Fdownload%2Fespree-6.2.1.tgz", | |||||
| "integrity": "sha1-d/xy4f10SiBSwg84pbV1gy6Cc0o=", | |||||
| "dev": true, | "dev": true, | ||||
| "requires": { | "requires": { | ||||
| "acorn": "^7.1.1", | "acorn": "^7.1.1", | ||||
| @@ -4531,9 +4571,9 @@ | |||||
| }, | }, | ||||
| "dependencies": { | "dependencies": { | ||||
| "acorn": { | "acorn": { | ||||
| "version": "7.2.0", | |||||
| "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.2.0.tgz", | |||||
| "integrity": "sha512-apwXVmYVpQ34m/i71vrApRrRKCWQnZZF1+npOD0WV5xZFfwWOmKGQ2RWlfdy9vWITsenisM8M0Qeq8agcFHNiQ==", | |||||
| "version": "7.4.0", | |||||
| "resolved": "https://registry.npm.taobao.org/acorn/download/acorn-7.4.0.tgz?cache=0&sync_timestamp=1597235774928&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Facorn%2Fdownload%2Facorn-7.4.0.tgz", | |||||
| "integrity": "sha1-4a1IbmxUUBY0xsOXxcEh2qODYHw=", | |||||
| "dev": true | "dev": true | ||||
| } | } | ||||
| } | } | ||||
| @@ -4545,17 +4585,17 @@ | |||||
| }, | }, | ||||
| "esquery": { | "esquery": { | ||||
| "version": "1.3.1", | "version": "1.3.1", | ||||
| "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", | |||||
| "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", | |||||
| "resolved": "https://registry.npm.taobao.org/esquery/download/esquery-1.3.1.tgz", | |||||
| "integrity": "sha1-t4tYKKqOIU4p+3TE1bdS4cAz2lc=", | |||||
| "dev": true, | "dev": true, | ||||
| "requires": { | "requires": { | ||||
| "estraverse": "^5.1.0" | "estraverse": "^5.1.0" | ||||
| }, | }, | ||||
| "dependencies": { | "dependencies": { | ||||
| "estraverse": { | "estraverse": { | ||||
| "version": "5.1.0", | |||||
| "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.1.0.tgz", | |||||
| "integrity": "sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==", | |||||
| "version": "5.2.0", | |||||
| "resolved": "https://registry.npm.taobao.org/estraverse/download/estraverse-5.2.0.tgz?cache=0&sync_timestamp=1596642998635&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Festraverse%2Fdownload%2Festraverse-5.2.0.tgz", | |||||
| "integrity": "sha1-MH30JUfmzHMk088DwVXVzbjFOIA=", | |||||
| "dev": true | "dev": true | ||||
| } | } | ||||
| } | } | ||||
| @@ -5172,6 +5212,29 @@ | |||||
| "readable-stream": "^2.3.6" | "readable-stream": "^2.3.6" | ||||
| } | } | ||||
| }, | }, | ||||
| "follow-redirects": { | |||||
| "version": "1.5.10", | |||||
| "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", | |||||
| "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", | |||||
| "requires": { | |||||
| "debug": "=3.1.0" | |||||
| }, | |||||
| "dependencies": { | |||||
| "debug": { | |||||
| "version": "3.1.0", | |||||
| "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", | |||||
| "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", | |||||
| "requires": { | |||||
| "ms": "2.0.0" | |||||
| } | |||||
| }, | |||||
| "ms": { | |||||
| "version": "2.0.0", | |||||
| "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", | |||||
| "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" | |||||
| } | |||||
| } | |||||
| }, | |||||
| "fomantic-ui": { | "fomantic-ui": { | ||||
| "version": "2.8.4", | "version": "2.8.4", | ||||
| "resolved": "https://registry.npmjs.org/fomantic-ui/-/fomantic-ui-2.8.4.tgz", | "resolved": "https://registry.npmjs.org/fomantic-ui/-/fomantic-ui-2.8.4.tgz", | ||||
| @@ -6878,8 +6941,8 @@ | |||||
| }, | }, | ||||
| "ignore": { | "ignore": { | ||||
| "version": "4.0.6", | "version": "4.0.6", | ||||
| "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", | |||||
| "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", | |||||
| "resolved": "https://registry.npm.taobao.org/ignore/download/ignore-4.0.6.tgz", | |||||
| "integrity": "sha1-dQ49tYYgh7RzfrrIIH/9HvJ7Jfw=", | |||||
| "dev": true | "dev": true | ||||
| }, | }, | ||||
| "image-size": { | "image-size": { | ||||
| @@ -9332,8 +9395,8 @@ | |||||
| }, | }, | ||||
| "optionator": { | "optionator": { | ||||
| "version": "0.8.3", | "version": "0.8.3", | ||||
| "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", | |||||
| "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", | |||||
| "resolved": "https://registry.npm.taobao.org/optionator/download/optionator-0.8.3.tgz", | |||||
| "integrity": "sha1-hPodA2/p08fiHZmIS2ARZ+yPtJU=", | |||||
| "dev": true, | "dev": true, | ||||
| "requires": { | "requires": { | ||||
| "deep-is": "~0.1.3", | "deep-is": "~0.1.3", | ||||
| @@ -10835,8 +10898,8 @@ | |||||
| }, | }, | ||||
| "progress": { | "progress": { | ||||
| "version": "2.0.3", | "version": "2.0.3", | ||||
| "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", | |||||
| "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", | |||||
| "resolved": "https://registry.npm.taobao.org/progress/download/progress-2.0.3.tgz", | |||||
| "integrity": "sha1-foz42PW48jnBvGi+tOt4Vn1XLvg=", | |||||
| "dev": true | "dev": true | ||||
| }, | }, | ||||
| "promise": { | "promise": { | ||||
| @@ -10944,9 +11007,9 @@ | |||||
| "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" | "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" | ||||
| }, | }, | ||||
| "qs": { | "qs": { | ||||
| "version": "6.5.2", | |||||
| "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", | |||||
| "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" | |||||
| "version": "6.9.4", | |||||
| "resolved": "https://registry.npm.taobao.org/qs/download/qs-6.9.4.tgz", | |||||
| "integrity": "sha1-kJCykNH5FyjTwi5UhDykSupatoc=" | |||||
| }, | }, | ||||
| "query-string": { | "query-string": { | ||||
| "version": "4.3.4", | "version": "4.3.4", | ||||
| @@ -11346,8 +11409,8 @@ | |||||
| }, | }, | ||||
| "regexpp": { | "regexpp": { | ||||
| "version": "2.0.1", | "version": "2.0.1", | ||||
| "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", | |||||
| "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", | |||||
| "resolved": "https://registry.npm.taobao.org/regexpp/download/regexpp-2.0.1.tgz", | |||||
| "integrity": "sha1-jRnTHPYySCtYkEn4KB+T28uk0H8=", | |||||
| "dev": true | "dev": true | ||||
| }, | }, | ||||
| "regexpu-core": { | "regexpu-core": { | ||||
| @@ -11578,6 +11641,14 @@ | |||||
| "tough-cookie": "~2.5.0", | "tough-cookie": "~2.5.0", | ||||
| "tunnel-agent": "^0.6.0", | "tunnel-agent": "^0.6.0", | ||||
| "uuid": "^3.3.2" | "uuid": "^3.3.2" | ||||
| }, | |||||
| "dependencies": { | |||||
| "qs": { | |||||
| "version": "6.5.2", | |||||
| "resolved": "https://registry.npm.taobao.org/qs/download/qs-6.5.2.tgz", | |||||
| "integrity": "sha1-yzroBuh0BERYTvFUzo7pjUA/PjY=", | |||||
| "optional": true | |||||
| } | |||||
| } | } | ||||
| }, | }, | ||||
| "require-dir": { | "require-dir": { | ||||
| @@ -12187,6 +12258,11 @@ | |||||
| "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", | "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", | ||||
| "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" | "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" | ||||
| }, | }, | ||||
| "spark-md5": { | |||||
| "version": "3.0.1", | |||||
| "resolved": "https://registry.npmjs.org/spark-md5/-/spark-md5-3.0.1.tgz", | |||||
| "integrity": "sha512-0tF3AGSD1ppQeuffsLDIOWlKUd3lS92tFxcsrh5Pe3ZphhnoK+oXIBTzOAThZCiuINZLvpiLH/1VS1/ANEJVig==" | |||||
| }, | |||||
| "sparkles": { | "sparkles": { | ||||
| "version": "1.0.1", | "version": "1.0.1", | ||||
| "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz", | "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz", | ||||
| @@ -13639,8 +13715,8 @@ | |||||
| }, | }, | ||||
| "type-fest": { | "type-fest": { | ||||
| "version": "0.8.1", | "version": "0.8.1", | ||||
| "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", | |||||
| "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", | |||||
| "resolved": "https://registry.npm.taobao.org/type-fest/download/type-fest-0.8.1.tgz", | |||||
| "integrity": "sha1-CeJJ696FHTseSNJ8EFREZn8XuD0=", | |||||
| "dev": true | "dev": true | ||||
| }, | }, | ||||
| "typedarray": { | "typedarray": { | ||||
| @@ -14273,6 +14349,32 @@ | |||||
| "v-tooltip": "^2.0.0-rc.32" | "v-tooltip": "^2.0.0-rc.32" | ||||
| } | } | ||||
| }, | }, | ||||
| "vue-eslint-parser": { | |||||
| "version": "7.1.0", | |||||
| "resolved": "https://registry.npm.taobao.org/vue-eslint-parser/download/vue-eslint-parser-7.1.0.tgz?cache=0&sync_timestamp=1589684321779&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fvue-eslint-parser%2Fdownload%2Fvue-eslint-parser-7.1.0.tgz", | |||||
| "integrity": "sha1-nNvMgj5lawh1B6GRFzK4Z6wQHoM=", | |||||
| "dev": true, | |||||
| "requires": { | |||||
| "debug": "^4.1.1", | |||||
| "eslint-scope": "^5.0.0", | |||||
| "eslint-visitor-keys": "^1.1.0", | |||||
| "espree": "^6.2.1", | |||||
| "esquery": "^1.0.1", | |||||
| "lodash": "^4.17.15" | |||||
| }, | |||||
| "dependencies": { | |||||
| "eslint-scope": { | |||||
| "version": "5.1.0", | |||||
| "resolved": "https://registry.npm.taobao.org/eslint-scope/download/eslint-scope-5.1.0.tgz?cache=0&sync_timestamp=1591269986906&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Feslint-scope%2Fdownload%2Feslint-scope-5.1.0.tgz", | |||||
| "integrity": "sha1-0Plx3+WcaeDK2mhLI9Sdv4JgDOU=", | |||||
| "dev": true, | |||||
| "requires": { | |||||
| "esrecurse": "^4.1.0", | |||||
| "estraverse": "^4.1.1" | |||||
| } | |||||
| } | |||||
| } | |||||
| }, | |||||
| "vue-hot-reload-api": { | "vue-hot-reload-api": { | ||||
| "version": "2.3.4", | "version": "2.3.4", | ||||
| "resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz", | "resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz", | ||||
| @@ -14295,6 +14397,11 @@ | |||||
| "resolved": "https://registry.npmjs.org/vue-resize/-/vue-resize-0.4.5.tgz", | "resolved": "https://registry.npmjs.org/vue-resize/-/vue-resize-0.4.5.tgz", | ||||
| "integrity": "sha512-bhP7MlgJQ8TIkZJXAfDf78uJO+mEI3CaLABLjv0WNzr4CcGRGPIAItyWYnP6LsPA4Oq0WE+suidNs6dgpO4RHg==" | "integrity": "sha512-bhP7MlgJQ8TIkZJXAfDf78uJO+mEI3CaLABLjv0WNzr4CcGRGPIAItyWYnP6LsPA4Oq0WE+suidNs6dgpO4RHg==" | ||||
| }, | }, | ||||
| "vue-router": { | |||||
| "version": "3.3.4", | |||||
| "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.3.4.tgz", | |||||
| "integrity": "sha512-SdKRBeoXUjaZ9R/8AyxsdTqkOfMcI5tWxPZOUX5Ie1BTL5rPSZ0O++pbiZCeYeythiZIdLEfkDiQPKIaWk5hDg==" | |||||
| }, | |||||
| "vue-style-loader": { | "vue-style-loader": { | ||||
| "version": "4.1.2", | "version": "4.1.2", | ||||
| "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.2.tgz", | "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.2.tgz", | ||||
| @@ -14843,8 +14950,8 @@ | |||||
| }, | }, | ||||
| "word-wrap": { | "word-wrap": { | ||||
| "version": "1.2.3", | "version": "1.2.3", | ||||
| "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", | |||||
| "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", | |||||
| "resolved": "https://registry.npm.taobao.org/word-wrap/download/word-wrap-1.2.3.tgz?cache=0&sync_timestamp=1589683603678&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fword-wrap%2Fdownload%2Fword-wrap-1.2.3.tgz", | |||||
| "integrity": "sha1-YQY29rH3A4kb00dxzLF/uTtHB5w=", | |||||
| "dev": true | "dev": true | ||||
| }, | }, | ||||
| "wordwrap": { | "wordwrap": { | ||||
| @@ -11,13 +11,14 @@ | |||||
| "@babel/preset-env": "7.9.6", | "@babel/preset-env": "7.9.6", | ||||
| "@babel/runtime": "7.9.6", | "@babel/runtime": "7.9.6", | ||||
| "@primer/octicons": "9.6.0", | "@primer/octicons": "9.6.0", | ||||
| "axios": "0.19.2", | |||||
| "babel-loader": "8.1.0", | "babel-loader": "8.1.0", | ||||
| "clipboard": "2.0.6", | "clipboard": "2.0.6", | ||||
| "core-js": "3.6.5", | "core-js": "3.6.5", | ||||
| "css-loader": "3.5.3", | "css-loader": "3.5.3", | ||||
| "cssnano": "4.1.10", | "cssnano": "4.1.10", | ||||
| "domino": "2.1.5", | "domino": "2.1.5", | ||||
| "dropzone": "5.7.0", | |||||
| "dropzone": "5.7.2", | |||||
| "fast-glob": "3.2.2", | "fast-glob": "3.2.2", | ||||
| "file-loader": "6.0.0", | "file-loader": "6.0.0", | ||||
| "fomantic-ui": "2.8.4", | "fomantic-ui": "2.8.4", | ||||
| @@ -34,6 +35,8 @@ | |||||
| "postcss-loader": "3.0.0", | "postcss-loader": "3.0.0", | ||||
| "postcss-preset-env": "6.7.0", | "postcss-preset-env": "6.7.0", | ||||
| "postcss-safe-parser": "4.0.2", | "postcss-safe-parser": "4.0.2", | ||||
| "qs": "6.9.4", | |||||
| "spark-md5": "3.0.1", | |||||
| "svg-sprite-loader": "5.0.0", | "svg-sprite-loader": "5.0.0", | ||||
| "svgo": "1.3.2", | "svgo": "1.3.2", | ||||
| "svgo-loader": "2.2.1", | "svgo-loader": "2.2.1", | ||||
| @@ -43,6 +46,7 @@ | |||||
| "vue-bar-graph": "1.2.0", | "vue-bar-graph": "1.2.0", | ||||
| "vue-calendar-heatmap": "0.8.4", | "vue-calendar-heatmap": "0.8.4", | ||||
| "vue-loader": "15.9.2", | "vue-loader": "15.9.2", | ||||
| "vue-router": "3.3.4", | |||||
| "vue-template-compiler": "2.6.11", | "vue-template-compiler": "2.6.11", | ||||
| "webpack": "4.43.0", | "webpack": "4.43.0", | ||||
| "webpack-cli": "3.3.11", | "webpack-cli": "3.3.11", | ||||
| @@ -53,6 +57,7 @@ | |||||
| "eslint": "6.8.0", | "eslint": "6.8.0", | ||||
| "eslint-config-airbnb-base": "14.1.0", | "eslint-config-airbnb-base": "14.1.0", | ||||
| "eslint-plugin-import": "2.20.2", | "eslint-plugin-import": "2.20.2", | ||||
| "eslint-plugin-vue": "6.2.2", | |||||
| "stylelint": "13.3.3", | "stylelint": "13.3.3", | ||||
| "stylelint-config-standard": "20.0.0", | "stylelint-config-standard": "20.0.0", | ||||
| "updates": "10.2.11" | "updates": "10.2.11" | ||||
| @@ -37,6 +37,12 @@ const ( | |||||
| // Home render home page | // Home render home page | ||||
| func Home(ctx *context.Context) { | func Home(ctx *context.Context) { | ||||
| ctx.Data["PageIsHome"] = true | |||||
| ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled | |||||
| ctx.HTML(200, tplHome) | |||||
| } | |||||
| func Dashboard(ctx *context.Context) { | |||||
| if ctx.IsSigned { | if ctx.IsSigned { | ||||
| if !ctx.User.IsActive && setting.Service.RegisterEmailConfirm { | if !ctx.User.IsActive && setting.Service.RegisterEmailConfirm { | ||||
| ctx.Data["Title"] = ctx.Tr("auth.active_your_account") | ctx.Data["Title"] = ctx.Tr("auth.active_your_account") | ||||
| @@ -15,6 +15,7 @@ import ( | |||||
| "code.gitea.io/gitea/modules/auth/sso" | "code.gitea.io/gitea/modules/auth/sso" | ||||
| "code.gitea.io/gitea/modules/cache" | "code.gitea.io/gitea/modules/cache" | ||||
| "code.gitea.io/gitea/modules/cron" | "code.gitea.io/gitea/modules/cron" | ||||
| "code.gitea.io/gitea/modules/decompression" | |||||
| "code.gitea.io/gitea/modules/eventsource" | "code.gitea.io/gitea/modules/eventsource" | ||||
| "code.gitea.io/gitea/modules/git" | "code.gitea.io/gitea/modules/git" | ||||
| "code.gitea.io/gitea/modules/highlight" | "code.gitea.io/gitea/modules/highlight" | ||||
| @@ -61,6 +62,7 @@ func NewServices() { | |||||
| mailer.NewContext() | mailer.NewContext() | ||||
| _ = cache.NewContext() | _ = cache.NewContext() | ||||
| notification.NewContext() | notification.NewContext() | ||||
| decompression.NewContext() | |||||
| } | } | ||||
| // In case of problems connecting to DB, retry connection. Eg, PGSQL in Docker Container on Synology | // In case of problems connecting to DB, retry connection. Eg, PGSQL in Docker Container on Synology | ||||
| @@ -5,25 +5,33 @@ | |||||
| package repo | package repo | ||||
| import ( | import ( | ||||
| contexExt "context" | |||||
| "encoding/json" | |||||
| "fmt" | |||||
| "net/http" | |||||
| "strconv" | |||||
| "strings" | |||||
| "code.gitea.io/gitea/models" | "code.gitea.io/gitea/models" | ||||
| "code.gitea.io/gitea/modules/context" | "code.gitea.io/gitea/modules/context" | ||||
| "code.gitea.io/gitea/modules/log" | "code.gitea.io/gitea/modules/log" | ||||
| "code.gitea.io/gitea/modules/minio_ext" | |||||
| "code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
| "code.gitea.io/gitea/modules/storage" | "code.gitea.io/gitea/modules/storage" | ||||
| "code.gitea.io/gitea/modules/upload" | "code.gitea.io/gitea/modules/upload" | ||||
| "code.gitea.io/gitea/modules/worker" | "code.gitea.io/gitea/modules/worker" | ||||
| contexExt "context" | |||||
| "fmt" | |||||
| gouuid "github.com/satori/go.uuid" | |||||
| "net/http" | "net/http" | ||||
| "strconv" | "strconv" | ||||
| "strings" | "strings" | ||||
| gouuid "github.com/satori/go.uuid" | |||||
| ) | ) | ||||
| const ( | const ( | ||||
| //result of decompress | //result of decompress | ||||
| DecompressSuccess = "0" | |||||
| DecompressFailed = "1" | |||||
| DecompressSuccess = "0" | |||||
| DecompressFailed = "1" | |||||
| ) | ) | ||||
| func RenderAttachmentSettings(ctx *context.Context) { | func RenderAttachmentSettings(ctx *context.Context) { | ||||
| @@ -107,7 +115,7 @@ func DeleteAttachment(ctx *context.Context) { | |||||
| ctx.Error(403) | ctx.Error(403) | ||||
| return | return | ||||
| } | } | ||||
| err = models.DeleteAttachment(attach, true) | |||||
| err = models.DeleteAttachment(attach, false) | |||||
| if err != nil { | if err != nil { | ||||
| ctx.Error(500, fmt.Sprintf("DeleteAttachment: %v", err)) | ctx.Error(500, fmt.Sprintf("DeleteAttachment: %v", err)) | ||||
| return | return | ||||
| @@ -327,14 +335,229 @@ func UpdateAttachmentDecompressState(ctx *context.Context) { | |||||
| }) | }) | ||||
| } | } | ||||
| func GetSuccessChunks(ctx *context.Context) { | |||||
| fileMD5 := ctx.Query("md5") | |||||
| fileChunk, err := models.GetFileChunkByMD5AndUser(fileMD5, ctx.User.ID) | |||||
| if err != nil { | |||||
| if models.IsErrFileChunkNotExist(err) { | |||||
| ctx.JSON(200, map[string]string{ | |||||
| "uuid": "", | |||||
| "uploaded": "0", | |||||
| "uploadID": "", | |||||
| "chunks": "", | |||||
| }) | |||||
| } else { | |||||
| ctx.ServerError("GetFileChunkByMD5", err) | |||||
| } | |||||
| return | |||||
| } | |||||
| chunks, err := json.Marshal(fileChunk.CompletedParts) | |||||
| if err != nil { | |||||
| ctx.ServerError("json.Marshal failed", err) | |||||
| return | |||||
| } | |||||
| var attachID int64 | |||||
| attach, err := models.GetAttachmentByUUID(fileChunk.UUID) | |||||
| if err != nil { | |||||
| if models.IsErrAttachmentNotExist(err) { | |||||
| attachID = 0 | |||||
| } else { | |||||
| ctx.ServerError("GetAttachmentByUUID", err) | |||||
| return | |||||
| } | |||||
| } else { | |||||
| attachID = attach.ID | |||||
| } | |||||
| ctx.JSON(200, map[string]string{ | |||||
| "uuid": fileChunk.UUID, | |||||
| "uploaded": strconv.Itoa(fileChunk.IsUploaded), | |||||
| "uploadID": fileChunk.UploadID, | |||||
| "chunks": string(chunks), | |||||
| "attachID": strconv.Itoa(int(attachID)), | |||||
| }) | |||||
| } | |||||
| func NewMultipart(ctx *context.Context) { | |||||
| if !setting.Attachment.Enabled { | |||||
| ctx.Error(404, "attachment is not enabled") | |||||
| return | |||||
| } | |||||
| err := upload.VerifyFileType(ctx.Query("fileType"), strings.Split(setting.Attachment.AllowedTypes, ",")) | |||||
| if err != nil { | |||||
| ctx.Error(400, err.Error()) | |||||
| return | |||||
| } | |||||
| if setting.Attachment.StoreType == storage.MinioStorageType { | |||||
| totalChunkCounts := ctx.QueryInt("totalChunkCounts") | |||||
| if totalChunkCounts > minio_ext.MaxPartsCount { | |||||
| ctx.Error(400, fmt.Sprintf("chunk counts(%d) is too much", totalChunkCounts)) | |||||
| return | |||||
| } | |||||
| fileSize := ctx.QueryInt64("size") | |||||
| if fileSize > minio_ext.MaxMultipartPutObjectSize { | |||||
| ctx.Error(400, fmt.Sprintf("file size(%d) is too big", fileSize)) | |||||
| return | |||||
| } | |||||
| uuid := gouuid.NewV4().String() | |||||
| uploadID, err := storage.NewMultiPartUpload(uuid) | |||||
| if err != nil { | |||||
| ctx.ServerError("NewMultipart", err) | |||||
| return | |||||
| } | |||||
| _, err = models.InsertFileChunk(&models.FileChunk{ | |||||
| UUID: uuid, | |||||
| UserID: ctx.User.ID, | |||||
| UploadID: uploadID, | |||||
| Md5: ctx.Query("md5"), | |||||
| Size: fileSize, | |||||
| TotalChunks: totalChunkCounts, | |||||
| }) | |||||
| if err != nil { | |||||
| ctx.Error(500, fmt.Sprintf("InsertFileChunk: %v", err)) | |||||
| return | |||||
| } | |||||
| ctx.JSON(200, map[string]string{ | |||||
| "uuid": uuid, | |||||
| "uploadID": uploadID, | |||||
| }) | |||||
| } else { | |||||
| ctx.Error(404, "storage type is not enabled") | |||||
| return | |||||
| } | |||||
| } | |||||
| func GetMultipartUploadUrl(ctx *context.Context) { | |||||
| uuid := ctx.Query("uuid") | |||||
| uploadID := ctx.Query("uploadID") | |||||
| partNumber := ctx.QueryInt("chunkNumber") | |||||
| size := ctx.QueryInt64("size") | |||||
| if size > minio_ext.MinPartSize { | |||||
| ctx.Error(400, fmt.Sprintf("chunk size(%d) is too big", size)) | |||||
| return | |||||
| } | |||||
| url, err := storage.GenMultiPartSignedUrl(uuid, uploadID, partNumber, size) | |||||
| if err != nil { | |||||
| ctx.Error(500, fmt.Sprintf("GenMultiPartSignedUrl failed: %v", err)) | |||||
| return | |||||
| } | |||||
| ctx.JSON(200, map[string]string{ | |||||
| "url": url, | |||||
| }) | |||||
| } | |||||
| func CompleteMultipart(ctx *context.Context) { | |||||
| uuid := ctx.Query("uuid") | |||||
| uploadID := ctx.Query("uploadID") | |||||
| fileChunk, err := models.GetFileChunkByUUID(uuid) | |||||
| if err != nil { | |||||
| if models.IsErrFileChunkNotExist(err) { | |||||
| ctx.Error(404) | |||||
| } else { | |||||
| ctx.ServerError("GetFileChunkByUUID", err) | |||||
| } | |||||
| return | |||||
| } | |||||
| _, err = storage.CompleteMultiPartUpload(uuid, uploadID, fileChunk.CompletedParts) | |||||
| if err != nil { | |||||
| ctx.Error(500, fmt.Sprintf("CompleteMultiPartUpload failed: %v", err)) | |||||
| return | |||||
| } | |||||
| fileChunk.IsUploaded = models.FileUploaded | |||||
| err = models.UpdateFileChunk(fileChunk) | |||||
| if err != nil { | |||||
| ctx.Error(500, fmt.Sprintf("UpdateFileChunk: %v", err)) | |||||
| return | |||||
| } | |||||
| attachment, err := models.InsertAttachment(&models.Attachment{ | |||||
| UUID: uuid, | |||||
| UploaderID: ctx.User.ID, | |||||
| IsPrivate: true, | |||||
| Name: ctx.Query("file_name"), | |||||
| Size: ctx.QueryInt64("size"), | |||||
| DatasetID: ctx.QueryInt64("dataset_id"), | |||||
| }) | |||||
| if err != nil { | |||||
| ctx.Error(500, fmt.Sprintf("InsertAttachment: %v", err)) | |||||
| return | |||||
| } | |||||
| if attachment.DatasetID != 0 { | |||||
| if strings.HasSuffix(attachment.Name, ".zip") { | |||||
| err = worker.SendDecompressTask(contexExt.Background(), uuid) | |||||
| if err != nil { | |||||
| log.Error("SendDecompressTask(%s) failed:%s", uuid, err.Error()) | |||||
| } else { | |||||
| attachment.DecompressState = models.DecompressStateIng | |||||
| err = models.UpdateAttachment(attachment) | |||||
| if err != nil { | |||||
| log.Error("UpdateAttachment state(%s) failed:%s", uuid, err.Error()) | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| ctx.JSON(200, map[string]string{ | |||||
| "result_code": "0", | |||||
| }) | |||||
| } | |||||
| func UpdateMultipart(ctx *context.Context) { | |||||
| uuid := ctx.Query("uuid") | |||||
| partNumber := ctx.QueryInt("chunkNumber") | |||||
| etag := ctx.Query("etag") | |||||
| fileChunk, err := models.GetFileChunkByUUID(uuid) | |||||
| if err != nil { | |||||
| if models.IsErrFileChunkNotExist(err) { | |||||
| ctx.Error(404) | |||||
| } else { | |||||
| ctx.ServerError("GetFileChunkByUUID", err) | |||||
| } | |||||
| return | |||||
| } | |||||
| fileChunk.CompletedParts = append(fileChunk.CompletedParts, strconv.Itoa(partNumber)+"-"+strings.Replace(etag, "\"", "", -1)) | |||||
| err = models.UpdateFileChunk(fileChunk) | |||||
| if err != nil { | |||||
| ctx.Error(500, fmt.Sprintf("UpdateFileChunk: %v", err)) | |||||
| return | |||||
| } | |||||
| ctx.JSON(200, map[string]string{ | |||||
| "result_code": "0", | |||||
| }) | |||||
| } | |||||
| func HandleUnDecompressAttachment() { | func HandleUnDecompressAttachment() { | ||||
| attachs,err := models.GetUnDecompressAttachments() | |||||
| attachs, err := models.GetUnDecompressAttachments() | |||||
| if err != nil { | if err != nil { | ||||
| log.Error("GetUnDecompressAttachments failed:", err.Error()) | log.Error("GetUnDecompressAttachments failed:", err.Error()) | ||||
| return | return | ||||
| } | } | ||||
| for _,attach := range attachs { | |||||
| for _, attach := range attachs { | |||||
| err = worker.SendDecompressTask(contexExt.Background(), attach.UUID) | err = worker.SendDecompressTask(contexExt.Background(), attach.UUID) | ||||
| if err != nil { | if err != nil { | ||||
| log.Error("SendDecompressTask(%s) failed:%s", attach.UUID, err.Error()) | log.Error("SendDecompressTask(%s) failed:%s", attach.UUID, err.Error()) | ||||
| @@ -1,18 +1,15 @@ | |||||
| package repo | package repo | ||||
| import ( | import ( | ||||
| "net/url" | |||||
| "sort" | "sort" | ||||
| "code.gitea.io/gitea/modules/storage" | |||||
| "code.gitea.io/gitea/modules/setting" | |||||
| "code.gitea.io/gitea/models" | "code.gitea.io/gitea/models" | ||||
| "code.gitea.io/gitea/modules/auth" | "code.gitea.io/gitea/modules/auth" | ||||
| "code.gitea.io/gitea/modules/base" | "code.gitea.io/gitea/modules/base" | ||||
| "code.gitea.io/gitea/modules/context" | "code.gitea.io/gitea/modules/context" | ||||
| "code.gitea.io/gitea/modules/log" | "code.gitea.io/gitea/modules/log" | ||||
| gouuid "github.com/satori/go.uuid" | |||||
| ) | ) | ||||
| const ( | const ( | ||||
| @@ -82,18 +79,8 @@ func DatasetIndex(ctx *context.Context) { | |||||
| ctx.Data["dataset"] = dataset | ctx.Data["dataset"] = dataset | ||||
| ctx.Data["Attachments"] = attachments | ctx.Data["Attachments"] = attachments | ||||
| ctx.Data["IsOwner"] = true | ctx.Data["IsOwner"] = true | ||||
| uuid := gouuid.NewV4().String() | |||||
| tmpUrl, err := storage.Attachments.PresignedPutURL(models.AttachmentRelativePath(uuid)) | |||||
| if err != nil { | |||||
| ctx.ServerError("PresignedPutURL", err) | |||||
| } | |||||
| preUrl, err := url.QueryUnescape(tmpUrl) | |||||
| if err != nil { | |||||
| ctx.ServerError("QueryUnescape", err) | |||||
| } | |||||
| ctx.Data["StoreType"] = setting.Attachment.StoreType | |||||
| ctx.Data["uuid"] = uuid | |||||
| ctx.Data["url"] = preUrl | |||||
| renderAttachmentSettings(ctx) | renderAttachmentSettings(ctx) | ||||
| ctx.HTML(200, tplIndex) | ctx.HTML(200, tplIndex) | ||||
| @@ -285,6 +285,7 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||
| return "" | return "" | ||||
| }) | }) | ||||
| m.Get("/", routers.Home) | m.Get("/", routers.Home) | ||||
| m.Get("/dashboard", routers.Dashboard) | |||||
| m.Group("/explore", func() { | m.Group("/explore", func() { | ||||
| m.Get("", func(ctx *context.Context) { | m.Get("", func(ctx *context.Context) { | ||||
| ctx.Redirect(setting.AppSubURL + "/explore/repos") | ctx.Redirect(setting.AppSubURL + "/explore/repos") | ||||
| @@ -521,6 +522,11 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||
| m.Get("/get_pre_url", repo.GetPresignedPutObjectURL) | m.Get("/get_pre_url", repo.GetPresignedPutObjectURL) | ||||
| m.Post("/add", repo.AddAttachment) | m.Post("/add", repo.AddAttachment) | ||||
| m.Post("/private", repo.UpdatePublicAttachment) | m.Post("/private", repo.UpdatePublicAttachment) | ||||
| m.Get("/get_chunks", repo.GetSuccessChunks) | |||||
| m.Get("/new_multipart", repo.NewMultipart) | |||||
| m.Get("/get_multipart_url", repo.GetMultipartUploadUrl) | |||||
| m.Post("/complete_multipart", repo.CompleteMultipart) | |||||
| m.Post("/update_chunk", repo.UpdateMultipart) | |||||
| }, reqSignIn) | }, reqSignIn) | ||||
| m.Group("/attachments", func() { | m.Group("/attachments", func() { | ||||
| @@ -9,7 +9,7 @@ | |||||
| </div> | </div> | ||||
| {{if .IsSigned}} | {{if .IsSigned}} | ||||
| <a class="item {{if .PageIsDashboard}}active{{end}}" href="/">{{.i18n.Tr "dashboard"}}</a> | |||||
| <a class="item {{if .PageIsDashboard}}active{{end}}" href="/dashboard">{{.i18n.Tr "dashboard"}}</a> | |||||
| <div class="ui dropdown item"> | <div class="ui dropdown item"> | ||||
| {{.i18n.Tr "custom.head.community"}} | {{.i18n.Tr "custom.head.community"}} | ||||
| <i class="dropdown icon"></i> | <i class="dropdown icon"></i> | ||||
| @@ -128,6 +128,10 @@ | |||||
| </div> | </div> | ||||
| <div class="divider"></div> | <div class="divider"></div> | ||||
| <a class="item" href="{{AppSubUrl}}/dashboard"> | |||||
| {{svg "octicon-info" 16}} | |||||
| {{.i18n.Tr "your_dashboard"}}<!-- Your dashboard --> | |||||
| </a> | |||||
| <a class="item" href="{{AppSubUrl}}/{{.SignedUser.Name}}"> | <a class="item" href="{{AppSubUrl}}/{{.SignedUser.Name}}"> | ||||
| {{svg "octicon-person" 16}} | {{svg "octicon-person" 16}} | ||||
| {{.i18n.Tr "your_profile"}}<!-- Your profile --> | {{.i18n.Tr "your_profile"}}<!-- Your profile --> | ||||
| @@ -2,7 +2,8 @@ | |||||
| <div class="field required dataset-files"> | <div class="field required dataset-files"> | ||||
| <label>{{.i18n.Tr "dataset.file"}}</label> | <label>{{.i18n.Tr "dataset.file"}}</label> | ||||
| <div class="files"></div> | <div class="files"></div> | ||||
| <div class="ui dropzone" id="dataset" data-upload-url="{{.url}}" data-uuid="{{.uuid}}" data-add-url="{{AppSubUrl}}/attachments/add" data-accepts="{{.AttachmentAllowedTypes}}" data-remove-url="{{AppSubUrl}}/attachments/delete" data-csrf="{{.CsrfToken}}" dataset-id={{.dataset.ID}} data-max-file="100" data-dataset-id="{{.dataset.ID}}" data-max-size="{{.AttachmentMaxSize}}" data-default-message="{{.i18n.Tr "dropzone.default_message"}}" data-invalid-input-type="{{.i18n.Tr "dropzone.invalid_input_type"}}" data-file-too-big="{{.i18n.Tr "dropzone.file_too_big"}}" data-remove-file="{{.i18n.Tr "dropzone.remove_file"}}"> | |||||
| <div class="ui dropzone" id="dataset" data-upload-url="{{AppSubUrl}}/attachments" data-accepts="{{.AttachmentAllowedTypes}}" data-remove-url="{{AppSubUrl}}/attachments/delete" data-csrf="{{.CsrfToken}}" dataset-id={{.dataset.ID}} data-max-file="100" data-dataset-id="{{.dataset.ID}}" data-max-size="{{.AttachmentMaxSize}}" data-default-message="{{.i18n.Tr "dropzone.default_message"}}" data-invalid-input-type="{{.i18n.Tr "dropzone.invalid_input_type"}}" data-file-too-big="{{.i18n.Tr "dropzone.file_too_big"}}" data-remove-file="{{.i18n.Tr "dropzone.remove_file"}}"> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| @@ -3,15 +3,13 @@ | |||||
| {{template "repo/header" .}} | {{template "repo/header" .}} | ||||
| <form class="ui container" action="{{.Link}}" method="post"> | <form class="ui container" action="{{.Link}}" method="post"> | ||||
| <input name="id" value="{{.dataset.ID}}" type="hidden" /> | <input name="id" value="{{.dataset.ID}}" type="hidden" /> | ||||
| <div id="datasetId" datasetId="{{.dataset.ID}}"> | |||||
| {{.CsrfTokenHtml}} | {{.CsrfTokenHtml}} | ||||
| {{template "base/alert" .}} | {{template "base/alert" .}} | ||||
| <div class="ui stackable grid {{if .Error}}hide{{end}}" id="dataset-content"> | <div class="ui stackable grid {{if .Error}}hide{{end}}" id="dataset-content"> | ||||
| <div class="row"> | <div class="row"> | ||||
| <div class="column sixteen {{if .Permission.CanWrite $.UnitTypeDatasets}}twelve{{end}} wide"> | <div class="column sixteen {{if .Permission.CanWrite $.UnitTypeDatasets}}twelve{{end}} wide"> | ||||
| <h2>{{.dataset.Title}}</h2> | <h2>{{.dataset.Title}}</h2> | ||||
| <div id="dataset-desc"> | |||||
| <span class="no-description text-italic">{{.dataset.Description}}</span> | |||||
| </div> | |||||
| </div> | </div> | ||||
| {{if .Permission.CanWrite $.UnitTypeDatasets}} | {{if .Permission.CanWrite $.UnitTypeDatasets}} | ||||
| <div class="column four wide right aligned"> | <div class="column four wide right aligned"> | ||||
| @@ -21,6 +19,11 @@ | |||||
| </div> | </div> | ||||
| {{end}} | {{end}} | ||||
| </div> | </div> | ||||
| <div class="row"> | |||||
| <div class="column sixteen wide"> | |||||
| <span class="no-description text-italic">{{.dataset.Description}}</span> | |||||
| </div> | |||||
| </div> | |||||
| </div> | </div> | ||||
| <div class="ui grid form segment success {{if not .Error}}hide{{end}}" id="dataset-content-edit"> | <div class="ui grid form segment success {{if not .Error}}hide{{end}}" id="dataset-content-edit"> | ||||
| @@ -65,11 +68,38 @@ | |||||
| </div> | </div> | ||||
| <div class="dataset ui middle very relaxed page"> | <div class="dataset ui middle very relaxed page"> | ||||
| <div class="column"> | <div class="column"> | ||||
| {{if .Permission.CanWrite $.UnitTypeDatasets}} | |||||
| <form class="ui form" action="{{.Link}}" method="post"> | |||||
| {{template "repo/datasets/dataset" .}} | |||||
| </form> | |||||
| {{if .Permission.CanWrite $.UnitTypeDatasets}} | |||||
| <div style='display:none;' | |||||
| id="minioUploader-params" | |||||
| data-uuid="{{.uuid}}" | |||||
| data-add-url="{{AppSubUrl}}/attachments/add" | |||||
| data-accepts="{{.AttachmentAllowedTypes}}" | |||||
| data-remove-url="{{AppSubUrl}}/attachments/delete" | |||||
| data-csrf="{{.CsrfToken}}" | |||||
| dataset-id={{.dataset.ID}} | |||||
| data-max-file="100" | |||||
| data-dataset-id="{{.dataset.ID}}" | |||||
| data-max-size="{{.AttachmentMaxSize}}" | |||||
| data-default-message="{{.i18n.Tr "dropzone.default_message"}}" | |||||
| data-invalid-input-type="{{.i18n.Tr "dropzone.invalid_input_type"}}" | |||||
| data-file-too-big="{{.i18n.Tr "dropzone.file_too_big"}}" | |||||
| data-remove-file="{{.i18n.Tr "dropzone.remove_file"}}" | |||||
| data-file-status='{{.i18n.Tr "dropzone.file_status"}}' | |||||
| data-file-init-status='{{.i18n.Tr "dropzone.file_init_status"}}' | |||||
| data-waitting-uploading='{{.i18n.Tr "dropzone.waitting_uploading"}}' | |||||
| data-md5-computing='{{.i18n.Tr "dropzone.md5_computing"}}' | |||||
| data-loading-file='{{.i18n.Tr "dropzone.loading_file"}}' | |||||
| data-upload-complete='{{.i18n.Tr "dropzone.upload_complete"}}' | |||||
| data-uploading='{{.i18n.Tr "dropzone.uploading"}}' | |||||
| > | |||||
| </div> | |||||
| {{if eq .StoreType "minio"}} | |||||
| <div id="minioUploader"></div> | |||||
| {{else}} | |||||
| <div style="margin: 2em 0;"> {{.i18n.Tr "dropzone.enable_minio_support"}} </div> | |||||
| {{end}} | {{end}} | ||||
| {{end}} | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| @@ -0,0 +1,315 @@ | |||||
| // Copyright 2016 Google LLC | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // http://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| // Package iam supports the resource-specific operations of Google Cloud | |||||
| // IAM (Identity and Access Management) for the Google Cloud Libraries. | |||||
| // See https://cloud.google.com/iam for more about IAM. | |||||
| // | |||||
| // Users of the Google Cloud Libraries will typically not use this package | |||||
| // directly. Instead they will begin with some resource that supports IAM, like | |||||
| // a pubsub topic, and call its IAM method to get a Handle for that resource. | |||||
| package iam | |||||
| import ( | |||||
| "context" | |||||
| "fmt" | |||||
| "time" | |||||
| gax "github.com/googleapis/gax-go/v2" | |||||
| pb "google.golang.org/genproto/googleapis/iam/v1" | |||||
| "google.golang.org/grpc" | |||||
| "google.golang.org/grpc/codes" | |||||
| "google.golang.org/grpc/metadata" | |||||
| ) | |||||
| // client abstracts the IAMPolicy API to allow multiple implementations. | |||||
| type client interface { | |||||
| Get(ctx context.Context, resource string) (*pb.Policy, error) | |||||
| Set(ctx context.Context, resource string, p *pb.Policy) error | |||||
| Test(ctx context.Context, resource string, perms []string) ([]string, error) | |||||
| } | |||||
| // grpcClient implements client for the standard gRPC-based IAMPolicy service. | |||||
| type grpcClient struct { | |||||
| c pb.IAMPolicyClient | |||||
| } | |||||
| var withRetry = gax.WithRetry(func() gax.Retryer { | |||||
| return gax.OnCodes([]codes.Code{ | |||||
| codes.DeadlineExceeded, | |||||
| codes.Unavailable, | |||||
| }, gax.Backoff{ | |||||
| Initial: 100 * time.Millisecond, | |||||
| Max: 60 * time.Second, | |||||
| Multiplier: 1.3, | |||||
| }) | |||||
| }) | |||||
| func (g *grpcClient) Get(ctx context.Context, resource string) (*pb.Policy, error) { | |||||
| var proto *pb.Policy | |||||
| md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "resource", resource)) | |||||
| ctx = insertMetadata(ctx, md) | |||||
| err := gax.Invoke(ctx, func(ctx context.Context, _ gax.CallSettings) error { | |||||
| var err error | |||||
| proto, err = g.c.GetIamPolicy(ctx, &pb.GetIamPolicyRequest{Resource: resource}) | |||||
| return err | |||||
| }, withRetry) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return proto, nil | |||||
| } | |||||
| func (g *grpcClient) Set(ctx context.Context, resource string, p *pb.Policy) error { | |||||
| md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "resource", resource)) | |||||
| ctx = insertMetadata(ctx, md) | |||||
| return gax.Invoke(ctx, func(ctx context.Context, _ gax.CallSettings) error { | |||||
| _, err := g.c.SetIamPolicy(ctx, &pb.SetIamPolicyRequest{ | |||||
| Resource: resource, | |||||
| Policy: p, | |||||
| }) | |||||
| return err | |||||
| }, withRetry) | |||||
| } | |||||
| func (g *grpcClient) Test(ctx context.Context, resource string, perms []string) ([]string, error) { | |||||
| var res *pb.TestIamPermissionsResponse | |||||
| md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "resource", resource)) | |||||
| ctx = insertMetadata(ctx, md) | |||||
| err := gax.Invoke(ctx, func(ctx context.Context, _ gax.CallSettings) error { | |||||
| var err error | |||||
| res, err = g.c.TestIamPermissions(ctx, &pb.TestIamPermissionsRequest{ | |||||
| Resource: resource, | |||||
| Permissions: perms, | |||||
| }) | |||||
| return err | |||||
| }, withRetry) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return res.Permissions, nil | |||||
| } | |||||
| // A Handle provides IAM operations for a resource. | |||||
| type Handle struct { | |||||
| c client | |||||
| resource string | |||||
| } | |||||
| // InternalNewHandle is for use by the Google Cloud Libraries only. | |||||
| // | |||||
| // InternalNewHandle returns a Handle for resource. | |||||
| // The conn parameter refers to a server that must support the IAMPolicy service. | |||||
| func InternalNewHandle(conn *grpc.ClientConn, resource string) *Handle { | |||||
| return InternalNewHandleGRPCClient(pb.NewIAMPolicyClient(conn), resource) | |||||
| } | |||||
| // InternalNewHandleGRPCClient is for use by the Google Cloud Libraries only. | |||||
| // | |||||
| // InternalNewHandleClient returns a Handle for resource using the given | |||||
| // grpc service that implements IAM as a mixin | |||||
| func InternalNewHandleGRPCClient(c pb.IAMPolicyClient, resource string) *Handle { | |||||
| return InternalNewHandleClient(&grpcClient{c: c}, resource) | |||||
| } | |||||
| // InternalNewHandleClient is for use by the Google Cloud Libraries only. | |||||
| // | |||||
| // InternalNewHandleClient returns a Handle for resource using the given | |||||
| // client implementation. | |||||
| func InternalNewHandleClient(c client, resource string) *Handle { | |||||
| return &Handle{ | |||||
| c: c, | |||||
| resource: resource, | |||||
| } | |||||
| } | |||||
| // Policy retrieves the IAM policy for the resource. | |||||
| func (h *Handle) Policy(ctx context.Context) (*Policy, error) { | |||||
| proto, err := h.c.Get(ctx, h.resource) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return &Policy{InternalProto: proto}, nil | |||||
| } | |||||
| // SetPolicy replaces the resource's current policy with the supplied Policy. | |||||
| // | |||||
| // If policy was created from a prior call to Get, then the modification will | |||||
| // only succeed if the policy has not changed since the Get. | |||||
| func (h *Handle) SetPolicy(ctx context.Context, policy *Policy) error { | |||||
| return h.c.Set(ctx, h.resource, policy.InternalProto) | |||||
| } | |||||
| // TestPermissions returns the subset of permissions that the caller has on the resource. | |||||
| func (h *Handle) TestPermissions(ctx context.Context, permissions []string) ([]string, error) { | |||||
| return h.c.Test(ctx, h.resource, permissions) | |||||
| } | |||||
| // A RoleName is a name representing a collection of permissions. | |||||
| type RoleName string | |||||
| // Common role names. | |||||
| const ( | |||||
| Owner RoleName = "roles/owner" | |||||
| Editor RoleName = "roles/editor" | |||||
| Viewer RoleName = "roles/viewer" | |||||
| ) | |||||
| const ( | |||||
| // AllUsers is a special member that denotes all users, even unauthenticated ones. | |||||
| AllUsers = "allUsers" | |||||
| // AllAuthenticatedUsers is a special member that denotes all authenticated users. | |||||
| AllAuthenticatedUsers = "allAuthenticatedUsers" | |||||
| ) | |||||
| // A Policy is a list of Bindings representing roles | |||||
| // granted to members. | |||||
| // | |||||
| // The zero Policy is a valid policy with no bindings. | |||||
| type Policy struct { | |||||
| // TODO(jba): when type aliases are available, put Policy into an internal package | |||||
| // and provide an exported alias here. | |||||
| // This field is exported for use by the Google Cloud Libraries only. | |||||
| // It may become unexported in a future release. | |||||
| InternalProto *pb.Policy | |||||
| } | |||||
| // Members returns the list of members with the supplied role. | |||||
| // The return value should not be modified. Use Add and Remove | |||||
| // to modify the members of a role. | |||||
| func (p *Policy) Members(r RoleName) []string { | |||||
| b := p.binding(r) | |||||
| if b == nil { | |||||
| return nil | |||||
| } | |||||
| return b.Members | |||||
| } | |||||
| // HasRole reports whether member has role r. | |||||
| func (p *Policy) HasRole(member string, r RoleName) bool { | |||||
| return memberIndex(member, p.binding(r)) >= 0 | |||||
| } | |||||
| // Add adds member member to role r if it is not already present. | |||||
| // A new binding is created if there is no binding for the role. | |||||
| func (p *Policy) Add(member string, r RoleName) { | |||||
| b := p.binding(r) | |||||
| if b == nil { | |||||
| if p.InternalProto == nil { | |||||
| p.InternalProto = &pb.Policy{} | |||||
| } | |||||
| p.InternalProto.Bindings = append(p.InternalProto.Bindings, &pb.Binding{ | |||||
| Role: string(r), | |||||
| Members: []string{member}, | |||||
| }) | |||||
| return | |||||
| } | |||||
| if memberIndex(member, b) < 0 { | |||||
| b.Members = append(b.Members, member) | |||||
| return | |||||
| } | |||||
| } | |||||
| // Remove removes member from role r if it is present. | |||||
| func (p *Policy) Remove(member string, r RoleName) { | |||||
| bi := p.bindingIndex(r) | |||||
| if bi < 0 { | |||||
| return | |||||
| } | |||||
| bindings := p.InternalProto.Bindings | |||||
| b := bindings[bi] | |||||
| mi := memberIndex(member, b) | |||||
| if mi < 0 { | |||||
| return | |||||
| } | |||||
| // Order doesn't matter for bindings or members, so to remove, move the last item | |||||
| // into the removed spot and shrink the slice. | |||||
| if len(b.Members) == 1 { | |||||
| // Remove binding. | |||||
| last := len(bindings) - 1 | |||||
| bindings[bi] = bindings[last] | |||||
| bindings[last] = nil | |||||
| p.InternalProto.Bindings = bindings[:last] | |||||
| return | |||||
| } | |||||
| // Remove member. | |||||
| // TODO(jba): worry about multiple copies of m? | |||||
| last := len(b.Members) - 1 | |||||
| b.Members[mi] = b.Members[last] | |||||
| b.Members[last] = "" | |||||
| b.Members = b.Members[:last] | |||||
| } | |||||
| // Roles returns the names of all the roles that appear in the Policy. | |||||
| func (p *Policy) Roles() []RoleName { | |||||
| if p.InternalProto == nil { | |||||
| return nil | |||||
| } | |||||
| var rns []RoleName | |||||
| for _, b := range p.InternalProto.Bindings { | |||||
| rns = append(rns, RoleName(b.Role)) | |||||
| } | |||||
| return rns | |||||
| } | |||||
| // binding returns the Binding for the suppied role, or nil if there isn't one. | |||||
| func (p *Policy) binding(r RoleName) *pb.Binding { | |||||
| i := p.bindingIndex(r) | |||||
| if i < 0 { | |||||
| return nil | |||||
| } | |||||
| return p.InternalProto.Bindings[i] | |||||
| } | |||||
| func (p *Policy) bindingIndex(r RoleName) int { | |||||
| if p.InternalProto == nil { | |||||
| return -1 | |||||
| } | |||||
| for i, b := range p.InternalProto.Bindings { | |||||
| if b.Role == string(r) { | |||||
| return i | |||||
| } | |||||
| } | |||||
| return -1 | |||||
| } | |||||
| // memberIndex returns the index of m in b's Members, or -1 if not found. | |||||
| func memberIndex(m string, b *pb.Binding) int { | |||||
| if b == nil { | |||||
| return -1 | |||||
| } | |||||
| for i, mm := range b.Members { | |||||
| if mm == m { | |||||
| return i | |||||
| } | |||||
| } | |||||
| return -1 | |||||
| } | |||||
| // insertMetadata inserts metadata into the given context | |||||
| func insertMetadata(ctx context.Context, mds ...metadata.MD) context.Context { | |||||
| out, _ := metadata.FromOutgoingContext(ctx) | |||||
| out = out.Copy() | |||||
| for _, md := range mds { | |||||
| for k, v := range md { | |||||
| out[k] = append(out[k], v...) | |||||
| } | |||||
| } | |||||
| return metadata.NewOutgoingContext(ctx, out) | |||||
| } | |||||
| @@ -0,0 +1,108 @@ | |||||
| // Copyright 2016 Google LLC | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // http://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| // Package optional provides versions of primitive types that can | |||||
| // be nil. These are useful in methods that update some of an API object's | |||||
| // fields. | |||||
| package optional | |||||
| import ( | |||||
| "fmt" | |||||
| "strings" | |||||
| "time" | |||||
| ) | |||||
| type ( | |||||
| // Bool is either a bool or nil. | |||||
| Bool interface{} | |||||
| // String is either a string or nil. | |||||
| String interface{} | |||||
| // Int is either an int or nil. | |||||
| Int interface{} | |||||
| // Uint is either a uint or nil. | |||||
| Uint interface{} | |||||
| // Float64 is either a float64 or nil. | |||||
| Float64 interface{} | |||||
| // Duration is either a time.Duration or nil. | |||||
| Duration interface{} | |||||
| ) | |||||
| // ToBool returns its argument as a bool. | |||||
| // It panics if its argument is nil or not a bool. | |||||
| func ToBool(v Bool) bool { | |||||
| x, ok := v.(bool) | |||||
| if !ok { | |||||
| doPanic("Bool", v) | |||||
| } | |||||
| return x | |||||
| } | |||||
| // ToString returns its argument as a string. | |||||
| // It panics if its argument is nil or not a string. | |||||
| func ToString(v String) string { | |||||
| x, ok := v.(string) | |||||
| if !ok { | |||||
| doPanic("String", v) | |||||
| } | |||||
| return x | |||||
| } | |||||
| // ToInt returns its argument as an int. | |||||
| // It panics if its argument is nil or not an int. | |||||
| func ToInt(v Int) int { | |||||
| x, ok := v.(int) | |||||
| if !ok { | |||||
| doPanic("Int", v) | |||||
| } | |||||
| return x | |||||
| } | |||||
| // ToUint returns its argument as a uint. | |||||
| // It panics if its argument is nil or not a uint. | |||||
| func ToUint(v Uint) uint { | |||||
| x, ok := v.(uint) | |||||
| if !ok { | |||||
| doPanic("Uint", v) | |||||
| } | |||||
| return x | |||||
| } | |||||
| // ToFloat64 returns its argument as a float64. | |||||
| // It panics if its argument is nil or not a float64. | |||||
| func ToFloat64(v Float64) float64 { | |||||
| x, ok := v.(float64) | |||||
| if !ok { | |||||
| doPanic("Float64", v) | |||||
| } | |||||
| return x | |||||
| } | |||||
| // ToDuration returns its argument as a time.Duration. | |||||
| // It panics if its argument is nil or not a time.Duration. | |||||
| func ToDuration(v Duration) time.Duration { | |||||
| x, ok := v.(time.Duration) | |||||
| if !ok { | |||||
| doPanic("Duration", v) | |||||
| } | |||||
| return x | |||||
| } | |||||
| func doPanic(capType string, v interface{}) { | |||||
| panic(fmt.Sprintf("optional.%s value should be %s, got %T", capType, strings.ToLower(capType), v)) | |||||
| } | |||||
| @@ -0,0 +1,19 @@ | |||||
| #!/bin/bash | |||||
| # Copyright 2019 Google LLC | |||||
| # | |||||
| # Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| # you may not use this file except in compliance with the License. | |||||
| # You may obtain a copy of the License at | |||||
| # | |||||
| # http://www.apache.org/licenses/LICENSE-2.0 | |||||
| # | |||||
| # Unless required by applicable law or agreed to in writing, software | |||||
| # distributed under the License is distributed on an "AS IS" BASIS, | |||||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| # See the License for the specific language governing permissions and | |||||
| # limitations under the License. | |||||
| today=$(date +%Y%m%d) | |||||
| sed -i -r -e 's/const Repo = "([0-9]{8})"/const Repo = "'$today'"/' $GOFILE | |||||
| @@ -0,0 +1,71 @@ | |||||
| // Copyright 2016 Google LLC | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // http://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| //go:generate ./update_version.sh | |||||
| // Package version contains version information for Google Cloud Client | |||||
| // Libraries for Go, as reported in request headers. | |||||
| package version | |||||
| import ( | |||||
| "runtime" | |||||
| "strings" | |||||
| "unicode" | |||||
| ) | |||||
| // Repo is the current version of the client libraries in this | |||||
| // repo. It should be a date in YYYYMMDD format. | |||||
| const Repo = "20190802" | |||||
| // Go returns the Go runtime version. The returned string | |||||
| // has no whitespace. | |||||
| func Go() string { | |||||
| return goVersion | |||||
| } | |||||
| var goVersion = goVer(runtime.Version()) | |||||
| const develPrefix = "devel +" | |||||
| func goVer(s string) string { | |||||
| if strings.HasPrefix(s, develPrefix) { | |||||
| s = s[len(develPrefix):] | |||||
| if p := strings.IndexFunc(s, unicode.IsSpace); p >= 0 { | |||||
| s = s[:p] | |||||
| } | |||||
| return s | |||||
| } | |||||
| if strings.HasPrefix(s, "go1") { | |||||
| s = s[2:] | |||||
| var prerelease string | |||||
| if p := strings.IndexFunc(s, notSemverRune); p >= 0 { | |||||
| s, prerelease = s[:p], s[p:] | |||||
| } | |||||
| if strings.HasSuffix(s, ".") { | |||||
| s += "0" | |||||
| } else if strings.Count(s, ".") < 2 { | |||||
| s += ".0" | |||||
| } | |||||
| if prerelease != "" { | |||||
| s += "-" + prerelease | |||||
| } | |||||
| return s | |||||
| } | |||||
| return "" | |||||
| } | |||||
| func notSemverRune(r rune) bool { | |||||
| return !strings.ContainsRune("0123456789.", r) | |||||
| } | |||||
| @@ -0,0 +1,46 @@ | |||||
| ## Cloud Pub/Sub [](https://godoc.org/cloud.google.com/go/pubsub) | |||||
| - [About Cloud Pubsub](https://cloud.google.com/pubsub/) | |||||
| - [API documentation](https://cloud.google.com/pubsub/docs) | |||||
| - [Go client documentation](https://godoc.org/cloud.google.com/go/pubsub) | |||||
| - [Complete sample programs](https://github.com/GoogleCloudPlatform/golang-samples/tree/master/pubsub) | |||||
| ### Example Usage | |||||
| First create a `pubsub.Client` to use throughout your application: | |||||
| [snip]:# (pubsub-1) | |||||
| ```go | |||||
| client, err := pubsub.NewClient(ctx, "project-id") | |||||
| if err != nil { | |||||
| log.Fatal(err) | |||||
| } | |||||
| ``` | |||||
| Then use the client to publish and subscribe: | |||||
| [snip]:# (pubsub-2) | |||||
| ```go | |||||
| // Publish "hello world" on topic1. | |||||
| topic := client.Topic("topic1") | |||||
| res := topic.Publish(ctx, &pubsub.Message{ | |||||
| Data: []byte("hello world"), | |||||
| }) | |||||
| // The publish happens asynchronously. | |||||
| // Later, you can get the result from res: | |||||
| ... | |||||
| msgID, err := res.Get(ctx) | |||||
| if err != nil { | |||||
| log.Fatal(err) | |||||
| } | |||||
| // Use a callback to receive messages via subscription1. | |||||
| sub := client.Subscription("subscription1") | |||||
| err = sub.Receive(ctx, func(ctx context.Context, m *pubsub.Message) { | |||||
| fmt.Println(m.Data) | |||||
| m.Ack() // Acknowledge that we've consumed the message. | |||||
| }) | |||||
| if err != nil { | |||||
| log.Println(err) | |||||
| } | |||||
| ``` | |||||
| @@ -0,0 +1,9 @@ | |||||
| Auto-generated pubsub v1 clients | |||||
| ================================= | |||||
| This package includes auto-generated clients for the pubsub v1 API. | |||||
| Use the handwritten client (in the parent directory, | |||||
| cloud.google.com/go/pubsub) in preference to this. | |||||
| This code is EXPERIMENTAL and subject to CHANGE AT ANY TIME. | |||||
| @@ -0,0 +1,103 @@ | |||||
| // Copyright 2019 Google LLC | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // https://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| // Code generated by gapic-generator. DO NOT EDIT. | |||||
| // Package pubsub is an auto-generated package for the | |||||
| // Google Cloud Pub/Sub API. | |||||
| // | |||||
| // Provides reliable, many-to-many, asynchronous messaging between | |||||
| // applications. | |||||
| // | |||||
| // Use of Context | |||||
| // | |||||
| // The ctx passed to NewClient is used for authentication requests and | |||||
| // for creating the underlying connection, but is not used for subsequent calls. | |||||
| // Individual methods on the client use the ctx given to them. | |||||
| // | |||||
| // To close the open connection, use the Close() method. | |||||
| // | |||||
| // For information about setting deadlines, reusing contexts, and more | |||||
| // please visit godoc.org/cloud.google.com/go. | |||||
| // | |||||
| // Use the client at cloud.google.com/go/pubsub in preference to this. | |||||
| package pubsub // import "cloud.google.com/go/pubsub/apiv1" | |||||
| import ( | |||||
| "context" | |||||
| "runtime" | |||||
| "strings" | |||||
| "unicode" | |||||
| "google.golang.org/grpc/metadata" | |||||
| ) | |||||
| func insertMetadata(ctx context.Context, mds ...metadata.MD) context.Context { | |||||
| out, _ := metadata.FromOutgoingContext(ctx) | |||||
| out = out.Copy() | |||||
| for _, md := range mds { | |||||
| for k, v := range md { | |||||
| out[k] = append(out[k], v...) | |||||
| } | |||||
| } | |||||
| return metadata.NewOutgoingContext(ctx, out) | |||||
| } | |||||
| // DefaultAuthScopes reports the default set of authentication scopes to use with this package. | |||||
| func DefaultAuthScopes() []string { | |||||
| return []string{ | |||||
| "https://www.googleapis.com/auth/cloud-platform", | |||||
| "https://www.googleapis.com/auth/pubsub", | |||||
| } | |||||
| } | |||||
| // versionGo returns the Go runtime version. The returned string | |||||
| // has no whitespace, suitable for reporting in header. | |||||
| func versionGo() string { | |||||
| const develPrefix = "devel +" | |||||
| s := runtime.Version() | |||||
| if strings.HasPrefix(s, develPrefix) { | |||||
| s = s[len(develPrefix):] | |||||
| if p := strings.IndexFunc(s, unicode.IsSpace); p >= 0 { | |||||
| s = s[:p] | |||||
| } | |||||
| return s | |||||
| } | |||||
| notSemverRune := func(r rune) bool { | |||||
| return strings.IndexRune("0123456789.", r) < 0 | |||||
| } | |||||
| if strings.HasPrefix(s, "go1") { | |||||
| s = s[2:] | |||||
| var prerelease string | |||||
| if p := strings.IndexFunc(s, notSemverRune); p >= 0 { | |||||
| s, prerelease = s[:p], s[p:] | |||||
| } | |||||
| if strings.HasSuffix(s, ".") { | |||||
| s += "0" | |||||
| } else if strings.Count(s, ".") < 2 { | |||||
| s += ".0" | |||||
| } | |||||
| if prerelease != "" { | |||||
| s += "-" + prerelease | |||||
| } | |||||
| return s | |||||
| } | |||||
| return "UNKNOWN" | |||||
| } | |||||
| const versionClient = "20190819" | |||||
| @@ -0,0 +1,36 @@ | |||||
| // Copyright 2018 Google LLC | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // https://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| package pubsub | |||||
| import ( | |||||
| "cloud.google.com/go/iam" | |||||
| pubsubpb "google.golang.org/genproto/googleapis/pubsub/v1" | |||||
| ) | |||||
| func (c *PublisherClient) SubscriptionIAM(subscription *pubsubpb.Subscription) *iam.Handle { | |||||
| return iam.InternalNewHandle(c.Connection(), subscription.Name) | |||||
| } | |||||
| func (c *PublisherClient) TopicIAM(topic *pubsubpb.Topic) *iam.Handle { | |||||
| return iam.InternalNewHandle(c.Connection(), topic.Name) | |||||
| } | |||||
| func (c *SubscriberClient) SubscriptionIAM(subscription *pubsubpb.Subscription) *iam.Handle { | |||||
| return iam.InternalNewHandle(c.Connection(), subscription.Name) | |||||
| } | |||||
| func (c *SubscriberClient) TopicIAM(topic *pubsubpb.Topic) *iam.Handle { | |||||
| return iam.InternalNewHandle(c.Connection(), topic.Name) | |||||
| } | |||||
| @@ -0,0 +1,95 @@ | |||||
| // Copyright 2018 Google LLC | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // https://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| package pubsub | |||||
| // PublisherProjectPath returns the path for the project resource. | |||||
| // | |||||
| // Deprecated: Use | |||||
| // fmt.Sprintf("projects/%s", project) | |||||
| // instead. | |||||
| func PublisherProjectPath(project string) string { | |||||
| return "" + | |||||
| "projects/" + | |||||
| project + | |||||
| "" | |||||
| } | |||||
| // PublisherTopicPath returns the path for the topic resource. | |||||
| // | |||||
| // Deprecated: Use | |||||
| // fmt.Sprintf("projects/%s/topics/%s", project, topic) | |||||
| // instead. | |||||
| func PublisherTopicPath(project, topic string) string { | |||||
| return "" + | |||||
| "projects/" + | |||||
| project + | |||||
| "/topics/" + | |||||
| topic + | |||||
| "" | |||||
| } | |||||
| // SubscriberProjectPath returns the path for the project resource. | |||||
| // | |||||
| // Deprecated: Use | |||||
| // fmt.Sprintf("projects/%s", project) | |||||
| // instead. | |||||
| func SubscriberProjectPath(project string) string { | |||||
| return "" + | |||||
| "projects/" + | |||||
| project + | |||||
| "" | |||||
| } | |||||
| // SubscriberSnapshotPath returns the path for the snapshot resource. | |||||
| // | |||||
| // Deprecated: Use | |||||
| // fmt.Sprintf("projects/%s/snapshots/%s", project, snapshot) | |||||
| // instead. | |||||
| func SubscriberSnapshotPath(project, snapshot string) string { | |||||
| return "" + | |||||
| "projects/" + | |||||
| project + | |||||
| "/snapshots/" + | |||||
| snapshot + | |||||
| "" | |||||
| } | |||||
| // SubscriberSubscriptionPath returns the path for the subscription resource. | |||||
| // | |||||
| // Deprecated: Use | |||||
| // fmt.Sprintf("projects/%s/subscriptions/%s", project, subscription) | |||||
| // instead. | |||||
| func SubscriberSubscriptionPath(project, subscription string) string { | |||||
| return "" + | |||||
| "projects/" + | |||||
| project + | |||||
| "/subscriptions/" + | |||||
| subscription + | |||||
| "" | |||||
| } | |||||
| // SubscriberTopicPath returns the path for the topic resource. | |||||
| // | |||||
| // Deprecated: Use | |||||
| // fmt.Sprintf("projects/%s/topics/%s", project, topic) | |||||
| // instead. | |||||
| func SubscriberTopicPath(project, topic string) string { | |||||
| return "" + | |||||
| "projects/" + | |||||
| project + | |||||
| "/topics/" + | |||||
| topic + | |||||
| "" | |||||
| } | |||||
| @@ -0,0 +1,417 @@ | |||||
| // Copyright 2019 Google LLC | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // https://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| // Code generated by gapic-generator. DO NOT EDIT. | |||||
| package pubsub | |||||
| import ( | |||||
| "context" | |||||
| "fmt" | |||||
| "math" | |||||
| "net/url" | |||||
| "time" | |||||
| "github.com/golang/protobuf/proto" | |||||
| gax "github.com/googleapis/gax-go/v2" | |||||
| "google.golang.org/api/iterator" | |||||
| "google.golang.org/api/option" | |||||
| "google.golang.org/api/transport" | |||||
| pubsubpb "google.golang.org/genproto/googleapis/pubsub/v1" | |||||
| "google.golang.org/grpc" | |||||
| "google.golang.org/grpc/codes" | |||||
| "google.golang.org/grpc/metadata" | |||||
| ) | |||||
| // PublisherCallOptions contains the retry settings for each method of PublisherClient. | |||||
| type PublisherCallOptions struct { | |||||
| CreateTopic []gax.CallOption | |||||
| UpdateTopic []gax.CallOption | |||||
| Publish []gax.CallOption | |||||
| GetTopic []gax.CallOption | |||||
| ListTopics []gax.CallOption | |||||
| ListTopicSubscriptions []gax.CallOption | |||||
| DeleteTopic []gax.CallOption | |||||
| } | |||||
| func defaultPublisherClientOptions() []option.ClientOption { | |||||
| return []option.ClientOption{ | |||||
| option.WithEndpoint("pubsub.googleapis.com:443"), | |||||
| option.WithScopes(DefaultAuthScopes()...), | |||||
| option.WithGRPCDialOption(grpc.WithDefaultCallOptions( | |||||
| grpc.MaxCallRecvMsgSize(math.MaxInt32))), | |||||
| } | |||||
| } | |||||
| func defaultPublisherCallOptions() *PublisherCallOptions { | |||||
| retry := map[[2]string][]gax.CallOption{ | |||||
| {"default", "idempotent"}: { | |||||
| gax.WithRetry(func() gax.Retryer { | |||||
| return gax.OnCodes([]codes.Code{ | |||||
| codes.Aborted, | |||||
| codes.Unavailable, | |||||
| codes.Unknown, | |||||
| }, gax.Backoff{ | |||||
| Initial: 100 * time.Millisecond, | |||||
| Max: 60000 * time.Millisecond, | |||||
| Multiplier: 1.3, | |||||
| }) | |||||
| }), | |||||
| }, | |||||
| {"default", "non_idempotent"}: { | |||||
| gax.WithRetry(func() gax.Retryer { | |||||
| return gax.OnCodes([]codes.Code{ | |||||
| codes.Unavailable, | |||||
| }, gax.Backoff{ | |||||
| Initial: 100 * time.Millisecond, | |||||
| Max: 60000 * time.Millisecond, | |||||
| Multiplier: 1.3, | |||||
| }) | |||||
| }), | |||||
| }, | |||||
| {"messaging", "publish"}: { | |||||
| gax.WithRetry(func() gax.Retryer { | |||||
| return gax.OnCodes([]codes.Code{ | |||||
| codes.Aborted, | |||||
| codes.Canceled, | |||||
| codes.DeadlineExceeded, | |||||
| codes.Internal, | |||||
| codes.ResourceExhausted, | |||||
| codes.Unavailable, | |||||
| codes.Unknown, | |||||
| }, gax.Backoff{ | |||||
| Initial: 100 * time.Millisecond, | |||||
| Max: 60000 * time.Millisecond, | |||||
| Multiplier: 1.3, | |||||
| }) | |||||
| }), | |||||
| }, | |||||
| } | |||||
| return &PublisherCallOptions{ | |||||
| CreateTopic: retry[[2]string{"default", "non_idempotent"}], | |||||
| UpdateTopic: retry[[2]string{"default", "non_idempotent"}], | |||||
| Publish: retry[[2]string{"messaging", "publish"}], | |||||
| GetTopic: retry[[2]string{"default", "idempotent"}], | |||||
| ListTopics: retry[[2]string{"default", "idempotent"}], | |||||
| ListTopicSubscriptions: retry[[2]string{"default", "idempotent"}], | |||||
| DeleteTopic: retry[[2]string{"default", "non_idempotent"}], | |||||
| } | |||||
| } | |||||
| // PublisherClient is a client for interacting with Google Cloud Pub/Sub API. | |||||
| // | |||||
| // Methods, except Close, may be called concurrently. However, fields must not be modified concurrently with method calls. | |||||
| type PublisherClient struct { | |||||
| // The connection to the service. | |||||
| conn *grpc.ClientConn | |||||
| // The gRPC API client. | |||||
| publisherClient pubsubpb.PublisherClient | |||||
| // The call options for this service. | |||||
| CallOptions *PublisherCallOptions | |||||
| // The x-goog-* metadata to be sent with each request. | |||||
| xGoogMetadata metadata.MD | |||||
| } | |||||
| // NewPublisherClient creates a new publisher client. | |||||
| // | |||||
| // The service that an application uses to manipulate topics, and to send | |||||
| // messages to a topic. | |||||
| func NewPublisherClient(ctx context.Context, opts ...option.ClientOption) (*PublisherClient, error) { | |||||
| conn, err := transport.DialGRPC(ctx, append(defaultPublisherClientOptions(), opts...)...) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| c := &PublisherClient{ | |||||
| conn: conn, | |||||
| CallOptions: defaultPublisherCallOptions(), | |||||
| publisherClient: pubsubpb.NewPublisherClient(conn), | |||||
| } | |||||
| c.SetGoogleClientInfo() | |||||
| return c, nil | |||||
| } | |||||
| // Connection returns the client's connection to the API service. | |||||
| func (c *PublisherClient) Connection() *grpc.ClientConn { | |||||
| return c.conn | |||||
| } | |||||
| // Close closes the connection to the API service. The user should invoke this when | |||||
| // the client is no longer required. | |||||
| func (c *PublisherClient) Close() error { | |||||
| return c.conn.Close() | |||||
| } | |||||
| // SetGoogleClientInfo sets the name and version of the application in | |||||
| // the `x-goog-api-client` header passed on each request. Intended for | |||||
| // use by Google-written clients. | |||||
| func (c *PublisherClient) SetGoogleClientInfo(keyval ...string) { | |||||
| kv := append([]string{"gl-go", versionGo()}, keyval...) | |||||
| kv = append(kv, "gapic", versionClient, "gax", gax.Version, "grpc", grpc.Version) | |||||
| c.xGoogMetadata = metadata.Pairs("x-goog-api-client", gax.XGoogHeader(kv...)) | |||||
| } | |||||
| // CreateTopic creates the given topic with the given name. See the | |||||
| // <a href="https://cloud.google.com/pubsub/docs/admin#resource_names"> | |||||
| // resource name rules</a>. | |||||
| func (c *PublisherClient) CreateTopic(ctx context.Context, req *pubsubpb.Topic, opts ...gax.CallOption) (*pubsubpb.Topic, error) { | |||||
| md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "name", url.QueryEscape(req.GetName()))) | |||||
| ctx = insertMetadata(ctx, c.xGoogMetadata, md) | |||||
| opts = append(c.CallOptions.CreateTopic[0:len(c.CallOptions.CreateTopic):len(c.CallOptions.CreateTopic)], opts...) | |||||
| var resp *pubsubpb.Topic | |||||
| err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error { | |||||
| var err error | |||||
| resp, err = c.publisherClient.CreateTopic(ctx, req, settings.GRPC...) | |||||
| return err | |||||
| }, opts...) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return resp, nil | |||||
| } | |||||
| // UpdateTopic updates an existing topic. Note that certain properties of a | |||||
| // topic are not modifiable. | |||||
| func (c *PublisherClient) UpdateTopic(ctx context.Context, req *pubsubpb.UpdateTopicRequest, opts ...gax.CallOption) (*pubsubpb.Topic, error) { | |||||
| md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "topic.name", url.QueryEscape(req.GetTopic().GetName()))) | |||||
| ctx = insertMetadata(ctx, c.xGoogMetadata, md) | |||||
| opts = append(c.CallOptions.UpdateTopic[0:len(c.CallOptions.UpdateTopic):len(c.CallOptions.UpdateTopic)], opts...) | |||||
| var resp *pubsubpb.Topic | |||||
| err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error { | |||||
| var err error | |||||
| resp, err = c.publisherClient.UpdateTopic(ctx, req, settings.GRPC...) | |||||
| return err | |||||
| }, opts...) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return resp, nil | |||||
| } | |||||
| // Publish adds one or more messages to the topic. Returns NOT_FOUND if the topic | |||||
| // does not exist. | |||||
| func (c *PublisherClient) Publish(ctx context.Context, req *pubsubpb.PublishRequest, opts ...gax.CallOption) (*pubsubpb.PublishResponse, error) { | |||||
| md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "topic", url.QueryEscape(req.GetTopic()))) | |||||
| ctx = insertMetadata(ctx, c.xGoogMetadata, md) | |||||
| opts = append(c.CallOptions.Publish[0:len(c.CallOptions.Publish):len(c.CallOptions.Publish)], opts...) | |||||
| var resp *pubsubpb.PublishResponse | |||||
| err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error { | |||||
| var err error | |||||
| resp, err = c.publisherClient.Publish(ctx, req, settings.GRPC...) | |||||
| return err | |||||
| }, opts...) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return resp, nil | |||||
| } | |||||
| // GetTopic gets the configuration of a topic. | |||||
| func (c *PublisherClient) GetTopic(ctx context.Context, req *pubsubpb.GetTopicRequest, opts ...gax.CallOption) (*pubsubpb.Topic, error) { | |||||
| md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "topic", url.QueryEscape(req.GetTopic()))) | |||||
| ctx = insertMetadata(ctx, c.xGoogMetadata, md) | |||||
| opts = append(c.CallOptions.GetTopic[0:len(c.CallOptions.GetTopic):len(c.CallOptions.GetTopic)], opts...) | |||||
| var resp *pubsubpb.Topic | |||||
| err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error { | |||||
| var err error | |||||
| resp, err = c.publisherClient.GetTopic(ctx, req, settings.GRPC...) | |||||
| return err | |||||
| }, opts...) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return resp, nil | |||||
| } | |||||
| // ListTopics lists matching topics. | |||||
| func (c *PublisherClient) ListTopics(ctx context.Context, req *pubsubpb.ListTopicsRequest, opts ...gax.CallOption) *TopicIterator { | |||||
| md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "project", url.QueryEscape(req.GetProject()))) | |||||
| ctx = insertMetadata(ctx, c.xGoogMetadata, md) | |||||
| opts = append(c.CallOptions.ListTopics[0:len(c.CallOptions.ListTopics):len(c.CallOptions.ListTopics)], opts...) | |||||
| it := &TopicIterator{} | |||||
| req = proto.Clone(req).(*pubsubpb.ListTopicsRequest) | |||||
| it.InternalFetch = func(pageSize int, pageToken string) ([]*pubsubpb.Topic, string, error) { | |||||
| var resp *pubsubpb.ListTopicsResponse | |||||
| req.PageToken = pageToken | |||||
| if pageSize > math.MaxInt32 { | |||||
| req.PageSize = math.MaxInt32 | |||||
| } else { | |||||
| req.PageSize = int32(pageSize) | |||||
| } | |||||
| err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error { | |||||
| var err error | |||||
| resp, err = c.publisherClient.ListTopics(ctx, req, settings.GRPC...) | |||||
| return err | |||||
| }, opts...) | |||||
| if err != nil { | |||||
| return nil, "", err | |||||
| } | |||||
| return resp.Topics, resp.NextPageToken, nil | |||||
| } | |||||
| fetch := func(pageSize int, pageToken string) (string, error) { | |||||
| items, nextPageToken, err := it.InternalFetch(pageSize, pageToken) | |||||
| if err != nil { | |||||
| return "", err | |||||
| } | |||||
| it.items = append(it.items, items...) | |||||
| return nextPageToken, nil | |||||
| } | |||||
| it.pageInfo, it.nextFunc = iterator.NewPageInfo(fetch, it.bufLen, it.takeBuf) | |||||
| it.pageInfo.MaxSize = int(req.PageSize) | |||||
| it.pageInfo.Token = req.PageToken | |||||
| return it | |||||
| } | |||||
| // ListTopicSubscriptions lists the names of the subscriptions on this topic. | |||||
| func (c *PublisherClient) ListTopicSubscriptions(ctx context.Context, req *pubsubpb.ListTopicSubscriptionsRequest, opts ...gax.CallOption) *StringIterator { | |||||
| md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "topic", url.QueryEscape(req.GetTopic()))) | |||||
| ctx = insertMetadata(ctx, c.xGoogMetadata, md) | |||||
| opts = append(c.CallOptions.ListTopicSubscriptions[0:len(c.CallOptions.ListTopicSubscriptions):len(c.CallOptions.ListTopicSubscriptions)], opts...) | |||||
| it := &StringIterator{} | |||||
| req = proto.Clone(req).(*pubsubpb.ListTopicSubscriptionsRequest) | |||||
| it.InternalFetch = func(pageSize int, pageToken string) ([]string, string, error) { | |||||
| var resp *pubsubpb.ListTopicSubscriptionsResponse | |||||
| req.PageToken = pageToken | |||||
| if pageSize > math.MaxInt32 { | |||||
| req.PageSize = math.MaxInt32 | |||||
| } else { | |||||
| req.PageSize = int32(pageSize) | |||||
| } | |||||
| err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error { | |||||
| var err error | |||||
| resp, err = c.publisherClient.ListTopicSubscriptions(ctx, req, settings.GRPC...) | |||||
| return err | |||||
| }, opts...) | |||||
| if err != nil { | |||||
| return nil, "", err | |||||
| } | |||||
| return resp.Subscriptions, resp.NextPageToken, nil | |||||
| } | |||||
| fetch := func(pageSize int, pageToken string) (string, error) { | |||||
| items, nextPageToken, err := it.InternalFetch(pageSize, pageToken) | |||||
| if err != nil { | |||||
| return "", err | |||||
| } | |||||
| it.items = append(it.items, items...) | |||||
| return nextPageToken, nil | |||||
| } | |||||
| it.pageInfo, it.nextFunc = iterator.NewPageInfo(fetch, it.bufLen, it.takeBuf) | |||||
| it.pageInfo.MaxSize = int(req.PageSize) | |||||
| it.pageInfo.Token = req.PageToken | |||||
| return it | |||||
| } | |||||
| // DeleteTopic deletes the topic with the given name. Returns NOT_FOUND if the topic | |||||
| // does not exist. After a topic is deleted, a new topic may be created with | |||||
| // the same name; this is an entirely new topic with none of the old | |||||
| // configuration or subscriptions. Existing subscriptions to this topic are | |||||
| // not deleted, but their topic field is set to _deleted-topic_. | |||||
| func (c *PublisherClient) DeleteTopic(ctx context.Context, req *pubsubpb.DeleteTopicRequest, opts ...gax.CallOption) error { | |||||
| md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "topic", url.QueryEscape(req.GetTopic()))) | |||||
| ctx = insertMetadata(ctx, c.xGoogMetadata, md) | |||||
| opts = append(c.CallOptions.DeleteTopic[0:len(c.CallOptions.DeleteTopic):len(c.CallOptions.DeleteTopic)], opts...) | |||||
| err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error { | |||||
| var err error | |||||
| _, err = c.publisherClient.DeleteTopic(ctx, req, settings.GRPC...) | |||||
| return err | |||||
| }, opts...) | |||||
| return err | |||||
| } | |||||
| // StringIterator manages a stream of string. | |||||
| type StringIterator struct { | |||||
| items []string | |||||
| pageInfo *iterator.PageInfo | |||||
| nextFunc func() error | |||||
| // InternalFetch is for use by the Google Cloud Libraries only. | |||||
| // It is not part of the stable interface of this package. | |||||
| // | |||||
| // InternalFetch returns results from a single call to the underlying RPC. | |||||
| // The number of results is no greater than pageSize. | |||||
| // If there are no more results, nextPageToken is empty and err is nil. | |||||
| InternalFetch func(pageSize int, pageToken string) (results []string, nextPageToken string, err error) | |||||
| } | |||||
| // PageInfo supports pagination. See the google.golang.org/api/iterator package for details. | |||||
| func (it *StringIterator) PageInfo() *iterator.PageInfo { | |||||
| return it.pageInfo | |||||
| } | |||||
| // Next returns the next result. Its second return value is iterator.Done if there are no more | |||||
| // results. Once Next returns Done, all subsequent calls will return Done. | |||||
| func (it *StringIterator) Next() (string, error) { | |||||
| var item string | |||||
| if err := it.nextFunc(); err != nil { | |||||
| return item, err | |||||
| } | |||||
| item = it.items[0] | |||||
| it.items = it.items[1:] | |||||
| return item, nil | |||||
| } | |||||
| func (it *StringIterator) bufLen() int { | |||||
| return len(it.items) | |||||
| } | |||||
| func (it *StringIterator) takeBuf() interface{} { | |||||
| b := it.items | |||||
| it.items = nil | |||||
| return b | |||||
| } | |||||
| // TopicIterator manages a stream of *pubsubpb.Topic. | |||||
| type TopicIterator struct { | |||||
| items []*pubsubpb.Topic | |||||
| pageInfo *iterator.PageInfo | |||||
| nextFunc func() error | |||||
| // InternalFetch is for use by the Google Cloud Libraries only. | |||||
| // It is not part of the stable interface of this package. | |||||
| // | |||||
| // InternalFetch returns results from a single call to the underlying RPC. | |||||
| // The number of results is no greater than pageSize. | |||||
| // If there are no more results, nextPageToken is empty and err is nil. | |||||
| InternalFetch func(pageSize int, pageToken string) (results []*pubsubpb.Topic, nextPageToken string, err error) | |||||
| } | |||||
| // PageInfo supports pagination. See the google.golang.org/api/iterator package for details. | |||||
| func (it *TopicIterator) PageInfo() *iterator.PageInfo { | |||||
| return it.pageInfo | |||||
| } | |||||
| // Next returns the next result. Its second return value is iterator.Done if there are no more | |||||
| // results. Once Next returns Done, all subsequent calls will return Done. | |||||
| func (it *TopicIterator) Next() (*pubsubpb.Topic, error) { | |||||
| var item *pubsubpb.Topic | |||||
| if err := it.nextFunc(); err != nil { | |||||
| return item, err | |||||
| } | |||||
| item = it.items[0] | |||||
| it.items = it.items[1:] | |||||
| return item, nil | |||||
| } | |||||
| func (it *TopicIterator) bufLen() int { | |||||
| return len(it.items) | |||||
| } | |||||
| func (it *TopicIterator) takeBuf() interface{} { | |||||
| b := it.items | |||||
| it.items = nil | |||||
| return b | |||||
| } | |||||
| @@ -0,0 +1,635 @@ | |||||
| // Copyright 2019 Google LLC | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // https://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| // Code generated by gapic-generator. DO NOT EDIT. | |||||
| package pubsub | |||||
| import ( | |||||
| "context" | |||||
| "fmt" | |||||
| "math" | |||||
| "net/url" | |||||
| "time" | |||||
| "github.com/golang/protobuf/proto" | |||||
| gax "github.com/googleapis/gax-go/v2" | |||||
| "google.golang.org/api/iterator" | |||||
| "google.golang.org/api/option" | |||||
| "google.golang.org/api/transport" | |||||
| pubsubpb "google.golang.org/genproto/googleapis/pubsub/v1" | |||||
| "google.golang.org/grpc" | |||||
| "google.golang.org/grpc/codes" | |||||
| "google.golang.org/grpc/metadata" | |||||
| ) | |||||
| // SubscriberCallOptions contains the retry settings for each method of SubscriberClient. | |||||
| type SubscriberCallOptions struct { | |||||
| CreateSubscription []gax.CallOption | |||||
| GetSubscription []gax.CallOption | |||||
| UpdateSubscription []gax.CallOption | |||||
| ListSubscriptions []gax.CallOption | |||||
| DeleteSubscription []gax.CallOption | |||||
| ModifyAckDeadline []gax.CallOption | |||||
| Acknowledge []gax.CallOption | |||||
| Pull []gax.CallOption | |||||
| StreamingPull []gax.CallOption | |||||
| ModifyPushConfig []gax.CallOption | |||||
| ListSnapshots []gax.CallOption | |||||
| CreateSnapshot []gax.CallOption | |||||
| UpdateSnapshot []gax.CallOption | |||||
| DeleteSnapshot []gax.CallOption | |||||
| Seek []gax.CallOption | |||||
| } | |||||
| func defaultSubscriberClientOptions() []option.ClientOption { | |||||
| return []option.ClientOption{ | |||||
| option.WithEndpoint("pubsub.googleapis.com:443"), | |||||
| option.WithScopes(DefaultAuthScopes()...), | |||||
| option.WithGRPCDialOption(grpc.WithDefaultCallOptions( | |||||
| grpc.MaxCallRecvMsgSize(math.MaxInt32))), | |||||
| } | |||||
| } | |||||
| func defaultSubscriberCallOptions() *SubscriberCallOptions { | |||||
| retry := map[[2]string][]gax.CallOption{ | |||||
| {"default", "idempotent"}: { | |||||
| gax.WithRetry(func() gax.Retryer { | |||||
| return gax.OnCodes([]codes.Code{ | |||||
| codes.Aborted, | |||||
| codes.Unavailable, | |||||
| codes.Unknown, | |||||
| }, gax.Backoff{ | |||||
| Initial: 100 * time.Millisecond, | |||||
| Max: 60000 * time.Millisecond, | |||||
| Multiplier: 1.3, | |||||
| }) | |||||
| }), | |||||
| }, | |||||
| {"default", "non_idempotent"}: { | |||||
| gax.WithRetry(func() gax.Retryer { | |||||
| return gax.OnCodes([]codes.Code{ | |||||
| codes.Unavailable, | |||||
| }, gax.Backoff{ | |||||
| Initial: 100 * time.Millisecond, | |||||
| Max: 60000 * time.Millisecond, | |||||
| Multiplier: 1.3, | |||||
| }) | |||||
| }), | |||||
| }, | |||||
| {"messaging", "idempotent"}: { | |||||
| gax.WithRetry(func() gax.Retryer { | |||||
| return gax.OnCodes([]codes.Code{ | |||||
| codes.Aborted, | |||||
| codes.Unavailable, | |||||
| codes.Unknown, | |||||
| }, gax.Backoff{ | |||||
| Initial: 100 * time.Millisecond, | |||||
| Max: 60000 * time.Millisecond, | |||||
| Multiplier: 1.3, | |||||
| }) | |||||
| }), | |||||
| }, | |||||
| {"messaging", "non_idempotent"}: { | |||||
| gax.WithRetry(func() gax.Retryer { | |||||
| return gax.OnCodes([]codes.Code{ | |||||
| codes.Unavailable, | |||||
| }, gax.Backoff{ | |||||
| Initial: 100 * time.Millisecond, | |||||
| Max: 60000 * time.Millisecond, | |||||
| Multiplier: 1.3, | |||||
| }) | |||||
| }), | |||||
| }, | |||||
| } | |||||
| return &SubscriberCallOptions{ | |||||
| CreateSubscription: retry[[2]string{"default", "idempotent"}], | |||||
| GetSubscription: retry[[2]string{"default", "idempotent"}], | |||||
| UpdateSubscription: retry[[2]string{"default", "non_idempotent"}], | |||||
| ListSubscriptions: retry[[2]string{"default", "idempotent"}], | |||||
| DeleteSubscription: retry[[2]string{"default", "non_idempotent"}], | |||||
| ModifyAckDeadline: retry[[2]string{"default", "non_idempotent"}], | |||||
| Acknowledge: retry[[2]string{"messaging", "non_idempotent"}], | |||||
| Pull: retry[[2]string{"messaging", "idempotent"}], | |||||
| StreamingPull: retry[[2]string{"streaming_messaging", "none"}], | |||||
| ModifyPushConfig: retry[[2]string{"default", "non_idempotent"}], | |||||
| ListSnapshots: retry[[2]string{"default", "idempotent"}], | |||||
| CreateSnapshot: retry[[2]string{"default", "non_idempotent"}], | |||||
| UpdateSnapshot: retry[[2]string{"default", "non_idempotent"}], | |||||
| DeleteSnapshot: retry[[2]string{"default", "non_idempotent"}], | |||||
| Seek: retry[[2]string{"default", "idempotent"}], | |||||
| } | |||||
| } | |||||
| // SubscriberClient is a client for interacting with Google Cloud Pub/Sub API. | |||||
| // | |||||
| // Methods, except Close, may be called concurrently. However, fields must not be modified concurrently with method calls. | |||||
| type SubscriberClient struct { | |||||
| // The connection to the service. | |||||
| conn *grpc.ClientConn | |||||
| // The gRPC API client. | |||||
| subscriberClient pubsubpb.SubscriberClient | |||||
| // The call options for this service. | |||||
| CallOptions *SubscriberCallOptions | |||||
| // The x-goog-* metadata to be sent with each request. | |||||
| xGoogMetadata metadata.MD | |||||
| } | |||||
| // NewSubscriberClient creates a new subscriber client. | |||||
| // | |||||
| // The service that an application uses to manipulate subscriptions and to | |||||
| // consume messages from a subscription via the Pull method or by | |||||
| // establishing a bi-directional stream using the StreamingPull method. | |||||
| func NewSubscriberClient(ctx context.Context, opts ...option.ClientOption) (*SubscriberClient, error) { | |||||
| conn, err := transport.DialGRPC(ctx, append(defaultSubscriberClientOptions(), opts...)...) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| c := &SubscriberClient{ | |||||
| conn: conn, | |||||
| CallOptions: defaultSubscriberCallOptions(), | |||||
| subscriberClient: pubsubpb.NewSubscriberClient(conn), | |||||
| } | |||||
| c.SetGoogleClientInfo() | |||||
| return c, nil | |||||
| } | |||||
| // Connection returns the client's connection to the API service. | |||||
| func (c *SubscriberClient) Connection() *grpc.ClientConn { | |||||
| return c.conn | |||||
| } | |||||
| // Close closes the connection to the API service. The user should invoke this when | |||||
| // the client is no longer required. | |||||
| func (c *SubscriberClient) Close() error { | |||||
| return c.conn.Close() | |||||
| } | |||||
| // SetGoogleClientInfo sets the name and version of the application in | |||||
| // the `x-goog-api-client` header passed on each request. Intended for | |||||
| // use by Google-written clients. | |||||
| func (c *SubscriberClient) SetGoogleClientInfo(keyval ...string) { | |||||
| kv := append([]string{"gl-go", versionGo()}, keyval...) | |||||
| kv = append(kv, "gapic", versionClient, "gax", gax.Version, "grpc", grpc.Version) | |||||
| c.xGoogMetadata = metadata.Pairs("x-goog-api-client", gax.XGoogHeader(kv...)) | |||||
| } | |||||
| // CreateSubscription creates a subscription to a given topic. See the | |||||
| // <a href="https://cloud.google.com/pubsub/docs/admin#resource_names"> | |||||
| // resource name rules</a>. | |||||
| // If the subscription already exists, returns ALREADY_EXISTS. | |||||
| // If the corresponding topic doesn't exist, returns NOT_FOUND. | |||||
| // | |||||
| // If the name is not provided in the request, the server will assign a random | |||||
| // name for this subscription on the same project as the topic, conforming | |||||
| // to the | |||||
| // resource name | |||||
| // format (at https://cloud.google.com/pubsub/docs/admin#resource_names). The | |||||
| // generated name is populated in the returned Subscription object. Note that | |||||
| // for REST API requests, you must specify a name in the request. | |||||
| func (c *SubscriberClient) CreateSubscription(ctx context.Context, req *pubsubpb.Subscription, opts ...gax.CallOption) (*pubsubpb.Subscription, error) { | |||||
| md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "name", url.QueryEscape(req.GetName()))) | |||||
| ctx = insertMetadata(ctx, c.xGoogMetadata, md) | |||||
| opts = append(c.CallOptions.CreateSubscription[0:len(c.CallOptions.CreateSubscription):len(c.CallOptions.CreateSubscription)], opts...) | |||||
| var resp *pubsubpb.Subscription | |||||
| err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error { | |||||
| var err error | |||||
| resp, err = c.subscriberClient.CreateSubscription(ctx, req, settings.GRPC...) | |||||
| return err | |||||
| }, opts...) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return resp, nil | |||||
| } | |||||
| // GetSubscription gets the configuration details of a subscription. | |||||
| func (c *SubscriberClient) GetSubscription(ctx context.Context, req *pubsubpb.GetSubscriptionRequest, opts ...gax.CallOption) (*pubsubpb.Subscription, error) { | |||||
| md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "subscription", url.QueryEscape(req.GetSubscription()))) | |||||
| ctx = insertMetadata(ctx, c.xGoogMetadata, md) | |||||
| opts = append(c.CallOptions.GetSubscription[0:len(c.CallOptions.GetSubscription):len(c.CallOptions.GetSubscription)], opts...) | |||||
| var resp *pubsubpb.Subscription | |||||
| err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error { | |||||
| var err error | |||||
| resp, err = c.subscriberClient.GetSubscription(ctx, req, settings.GRPC...) | |||||
| return err | |||||
| }, opts...) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return resp, nil | |||||
| } | |||||
| // UpdateSubscription updates an existing subscription. Note that certain properties of a | |||||
| // subscription, such as its topic, are not modifiable. | |||||
| func (c *SubscriberClient) UpdateSubscription(ctx context.Context, req *pubsubpb.UpdateSubscriptionRequest, opts ...gax.CallOption) (*pubsubpb.Subscription, error) { | |||||
| md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "subscription.name", url.QueryEscape(req.GetSubscription().GetName()))) | |||||
| ctx = insertMetadata(ctx, c.xGoogMetadata, md) | |||||
| opts = append(c.CallOptions.UpdateSubscription[0:len(c.CallOptions.UpdateSubscription):len(c.CallOptions.UpdateSubscription)], opts...) | |||||
| var resp *pubsubpb.Subscription | |||||
| err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error { | |||||
| var err error | |||||
| resp, err = c.subscriberClient.UpdateSubscription(ctx, req, settings.GRPC...) | |||||
| return err | |||||
| }, opts...) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return resp, nil | |||||
| } | |||||
| // ListSubscriptions lists matching subscriptions. | |||||
| func (c *SubscriberClient) ListSubscriptions(ctx context.Context, req *pubsubpb.ListSubscriptionsRequest, opts ...gax.CallOption) *SubscriptionIterator { | |||||
| md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "project", url.QueryEscape(req.GetProject()))) | |||||
| ctx = insertMetadata(ctx, c.xGoogMetadata, md) | |||||
| opts = append(c.CallOptions.ListSubscriptions[0:len(c.CallOptions.ListSubscriptions):len(c.CallOptions.ListSubscriptions)], opts...) | |||||
| it := &SubscriptionIterator{} | |||||
| req = proto.Clone(req).(*pubsubpb.ListSubscriptionsRequest) | |||||
| it.InternalFetch = func(pageSize int, pageToken string) ([]*pubsubpb.Subscription, string, error) { | |||||
| var resp *pubsubpb.ListSubscriptionsResponse | |||||
| req.PageToken = pageToken | |||||
| if pageSize > math.MaxInt32 { | |||||
| req.PageSize = math.MaxInt32 | |||||
| } else { | |||||
| req.PageSize = int32(pageSize) | |||||
| } | |||||
| err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error { | |||||
| var err error | |||||
| resp, err = c.subscriberClient.ListSubscriptions(ctx, req, settings.GRPC...) | |||||
| return err | |||||
| }, opts...) | |||||
| if err != nil { | |||||
| return nil, "", err | |||||
| } | |||||
| return resp.Subscriptions, resp.NextPageToken, nil | |||||
| } | |||||
| fetch := func(pageSize int, pageToken string) (string, error) { | |||||
| items, nextPageToken, err := it.InternalFetch(pageSize, pageToken) | |||||
| if err != nil { | |||||
| return "", err | |||||
| } | |||||
| it.items = append(it.items, items...) | |||||
| return nextPageToken, nil | |||||
| } | |||||
| it.pageInfo, it.nextFunc = iterator.NewPageInfo(fetch, it.bufLen, it.takeBuf) | |||||
| it.pageInfo.MaxSize = int(req.PageSize) | |||||
| it.pageInfo.Token = req.PageToken | |||||
| return it | |||||
| } | |||||
| // DeleteSubscription deletes an existing subscription. All messages retained in the subscription | |||||
| // are immediately dropped. Calls to Pull after deletion will return | |||||
| // NOT_FOUND. After a subscription is deleted, a new one may be created with | |||||
| // the same name, but the new one has no association with the old | |||||
| // subscription or its topic unless the same topic is specified. | |||||
| func (c *SubscriberClient) DeleteSubscription(ctx context.Context, req *pubsubpb.DeleteSubscriptionRequest, opts ...gax.CallOption) error { | |||||
| md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "subscription", url.QueryEscape(req.GetSubscription()))) | |||||
| ctx = insertMetadata(ctx, c.xGoogMetadata, md) | |||||
| opts = append(c.CallOptions.DeleteSubscription[0:len(c.CallOptions.DeleteSubscription):len(c.CallOptions.DeleteSubscription)], opts...) | |||||
| err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error { | |||||
| var err error | |||||
| _, err = c.subscriberClient.DeleteSubscription(ctx, req, settings.GRPC...) | |||||
| return err | |||||
| }, opts...) | |||||
| return err | |||||
| } | |||||
| // ModifyAckDeadline modifies the ack deadline for a specific message. This method is useful | |||||
| // to indicate that more time is needed to process a message by the | |||||
| // subscriber, or to make the message available for redelivery if the | |||||
| // processing was interrupted. Note that this does not modify the | |||||
| // subscription-level ackDeadlineSeconds used for subsequent messages. | |||||
| func (c *SubscriberClient) ModifyAckDeadline(ctx context.Context, req *pubsubpb.ModifyAckDeadlineRequest, opts ...gax.CallOption) error { | |||||
| md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "subscription", url.QueryEscape(req.GetSubscription()))) | |||||
| ctx = insertMetadata(ctx, c.xGoogMetadata, md) | |||||
| opts = append(c.CallOptions.ModifyAckDeadline[0:len(c.CallOptions.ModifyAckDeadline):len(c.CallOptions.ModifyAckDeadline)], opts...) | |||||
| err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error { | |||||
| var err error | |||||
| _, err = c.subscriberClient.ModifyAckDeadline(ctx, req, settings.GRPC...) | |||||
| return err | |||||
| }, opts...) | |||||
| return err | |||||
| } | |||||
| // Acknowledge acknowledges the messages associated with the ack_ids in the | |||||
| // AcknowledgeRequest. The Pub/Sub system can remove the relevant messages | |||||
| // from the subscription. | |||||
| // | |||||
| // Acknowledging a message whose ack deadline has expired may succeed, | |||||
| // but such a message may be redelivered later. Acknowledging a message more | |||||
| // than once will not result in an error. | |||||
| func (c *SubscriberClient) Acknowledge(ctx context.Context, req *pubsubpb.AcknowledgeRequest, opts ...gax.CallOption) error { | |||||
| md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "subscription", url.QueryEscape(req.GetSubscription()))) | |||||
| ctx = insertMetadata(ctx, c.xGoogMetadata, md) | |||||
| opts = append(c.CallOptions.Acknowledge[0:len(c.CallOptions.Acknowledge):len(c.CallOptions.Acknowledge)], opts...) | |||||
| err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error { | |||||
| var err error | |||||
| _, err = c.subscriberClient.Acknowledge(ctx, req, settings.GRPC...) | |||||
| return err | |||||
| }, opts...) | |||||
| return err | |||||
| } | |||||
| // Pull pulls messages from the server. The server may return UNAVAILABLE if | |||||
| // there are too many concurrent pull requests pending for the given | |||||
| // subscription. | |||||
| func (c *SubscriberClient) Pull(ctx context.Context, req *pubsubpb.PullRequest, opts ...gax.CallOption) (*pubsubpb.PullResponse, error) { | |||||
| md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "subscription", url.QueryEscape(req.GetSubscription()))) | |||||
| ctx = insertMetadata(ctx, c.xGoogMetadata, md) | |||||
| opts = append(c.CallOptions.Pull[0:len(c.CallOptions.Pull):len(c.CallOptions.Pull)], opts...) | |||||
| var resp *pubsubpb.PullResponse | |||||
| err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error { | |||||
| var err error | |||||
| resp, err = c.subscriberClient.Pull(ctx, req, settings.GRPC...) | |||||
| return err | |||||
| }, opts...) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return resp, nil | |||||
| } | |||||
| // StreamingPull establishes a stream with the server, which sends messages down to the | |||||
| // client. The client streams acknowledgements and ack deadline modifications | |||||
| // back to the server. The server will close the stream and return the status | |||||
| // on any error. The server may close the stream with status UNAVAILABLE to | |||||
| // reassign server-side resources, in which case, the client should | |||||
| // re-establish the stream. Flow control can be achieved by configuring the | |||||
| // underlying RPC channel. | |||||
| func (c *SubscriberClient) StreamingPull(ctx context.Context, opts ...gax.CallOption) (pubsubpb.Subscriber_StreamingPullClient, error) { | |||||
| ctx = insertMetadata(ctx, c.xGoogMetadata) | |||||
| opts = append(c.CallOptions.StreamingPull[0:len(c.CallOptions.StreamingPull):len(c.CallOptions.StreamingPull)], opts...) | |||||
| var resp pubsubpb.Subscriber_StreamingPullClient | |||||
| err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error { | |||||
| var err error | |||||
| resp, err = c.subscriberClient.StreamingPull(ctx, settings.GRPC...) | |||||
| return err | |||||
| }, opts...) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return resp, nil | |||||
| } | |||||
| // ModifyPushConfig modifies the PushConfig for a specified subscription. | |||||
| // | |||||
| // This may be used to change a push subscription to a pull one (signified by | |||||
| // an empty PushConfig) or vice versa, or change the endpoint URL and other | |||||
| // attributes of a push subscription. Messages will accumulate for delivery | |||||
| // continuously through the call regardless of changes to the PushConfig. | |||||
| func (c *SubscriberClient) ModifyPushConfig(ctx context.Context, req *pubsubpb.ModifyPushConfigRequest, opts ...gax.CallOption) error { | |||||
| md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "subscription", url.QueryEscape(req.GetSubscription()))) | |||||
| ctx = insertMetadata(ctx, c.xGoogMetadata, md) | |||||
| opts = append(c.CallOptions.ModifyPushConfig[0:len(c.CallOptions.ModifyPushConfig):len(c.CallOptions.ModifyPushConfig)], opts...) | |||||
| err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error { | |||||
| var err error | |||||
| _, err = c.subscriberClient.ModifyPushConfig(ctx, req, settings.GRPC...) | |||||
| return err | |||||
| }, opts...) | |||||
| return err | |||||
| } | |||||
| // ListSnapshots lists the existing snapshots. Snapshots are used in | |||||
| // <a href="https://cloud.google.com/pubsub/docs/replay-overview">Seek</a> | |||||
| // operations, which allow | |||||
| // you to manage message acknowledgments in bulk. That is, you can set the | |||||
| // acknowledgment state of messages in an existing subscription to the state | |||||
| // captured by a snapshot. | |||||
| func (c *SubscriberClient) ListSnapshots(ctx context.Context, req *pubsubpb.ListSnapshotsRequest, opts ...gax.CallOption) *SnapshotIterator { | |||||
| md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "project", url.QueryEscape(req.GetProject()))) | |||||
| ctx = insertMetadata(ctx, c.xGoogMetadata, md) | |||||
| opts = append(c.CallOptions.ListSnapshots[0:len(c.CallOptions.ListSnapshots):len(c.CallOptions.ListSnapshots)], opts...) | |||||
| it := &SnapshotIterator{} | |||||
| req = proto.Clone(req).(*pubsubpb.ListSnapshotsRequest) | |||||
| it.InternalFetch = func(pageSize int, pageToken string) ([]*pubsubpb.Snapshot, string, error) { | |||||
| var resp *pubsubpb.ListSnapshotsResponse | |||||
| req.PageToken = pageToken | |||||
| if pageSize > math.MaxInt32 { | |||||
| req.PageSize = math.MaxInt32 | |||||
| } else { | |||||
| req.PageSize = int32(pageSize) | |||||
| } | |||||
| err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error { | |||||
| var err error | |||||
| resp, err = c.subscriberClient.ListSnapshots(ctx, req, settings.GRPC...) | |||||
| return err | |||||
| }, opts...) | |||||
| if err != nil { | |||||
| return nil, "", err | |||||
| } | |||||
| return resp.Snapshots, resp.NextPageToken, nil | |||||
| } | |||||
| fetch := func(pageSize int, pageToken string) (string, error) { | |||||
| items, nextPageToken, err := it.InternalFetch(pageSize, pageToken) | |||||
| if err != nil { | |||||
| return "", err | |||||
| } | |||||
| it.items = append(it.items, items...) | |||||
| return nextPageToken, nil | |||||
| } | |||||
| it.pageInfo, it.nextFunc = iterator.NewPageInfo(fetch, it.bufLen, it.takeBuf) | |||||
| it.pageInfo.MaxSize = int(req.PageSize) | |||||
| it.pageInfo.Token = req.PageToken | |||||
| return it | |||||
| } | |||||
| // CreateSnapshot creates a snapshot from the requested subscription. Snapshots are used in | |||||
| // <a href="https://cloud.google.com/pubsub/docs/replay-overview">Seek</a> | |||||
| // operations, which allow | |||||
| // you to manage message acknowledgments in bulk. That is, you can set the | |||||
| // acknowledgment state of messages in an existing subscription to the state | |||||
| // captured by a snapshot. | |||||
| // <br><br>If the snapshot already exists, returns ALREADY_EXISTS. | |||||
| // If the requested subscription doesn't exist, returns NOT_FOUND. | |||||
| // If the backlog in the subscription is too old -- and the resulting snapshot | |||||
| // would expire in less than 1 hour -- then FAILED_PRECONDITION is returned. | |||||
| // See also the Snapshot.expire_time field. If the name is not provided in | |||||
| // the request, the server will assign a random | |||||
| // name for this snapshot on the same project as the subscription, conforming | |||||
| // to the | |||||
| // resource name | |||||
| // format (at https://cloud.google.com/pubsub/docs/admin#resource_names). The | |||||
| // generated name is populated in the returned Snapshot object. Note that for | |||||
| // REST API requests, you must specify a name in the request. | |||||
| func (c *SubscriberClient) CreateSnapshot(ctx context.Context, req *pubsubpb.CreateSnapshotRequest, opts ...gax.CallOption) (*pubsubpb.Snapshot, error) { | |||||
| md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "name", url.QueryEscape(req.GetName()))) | |||||
| ctx = insertMetadata(ctx, c.xGoogMetadata, md) | |||||
| opts = append(c.CallOptions.CreateSnapshot[0:len(c.CallOptions.CreateSnapshot):len(c.CallOptions.CreateSnapshot)], opts...) | |||||
| var resp *pubsubpb.Snapshot | |||||
| err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error { | |||||
| var err error | |||||
| resp, err = c.subscriberClient.CreateSnapshot(ctx, req, settings.GRPC...) | |||||
| return err | |||||
| }, opts...) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return resp, nil | |||||
| } | |||||
| // UpdateSnapshot updates an existing snapshot. Snapshots are used in | |||||
| // <a href="https://cloud.google.com/pubsub/docs/replay-overview">Seek</a> | |||||
| // operations, which allow | |||||
| // you to manage message acknowledgments in bulk. That is, you can set the | |||||
| // acknowledgment state of messages in an existing subscription to the state | |||||
| // captured by a snapshot. | |||||
| func (c *SubscriberClient) UpdateSnapshot(ctx context.Context, req *pubsubpb.UpdateSnapshotRequest, opts ...gax.CallOption) (*pubsubpb.Snapshot, error) { | |||||
| md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "snapshot.name", url.QueryEscape(req.GetSnapshot().GetName()))) | |||||
| ctx = insertMetadata(ctx, c.xGoogMetadata, md) | |||||
| opts = append(c.CallOptions.UpdateSnapshot[0:len(c.CallOptions.UpdateSnapshot):len(c.CallOptions.UpdateSnapshot)], opts...) | |||||
| var resp *pubsubpb.Snapshot | |||||
| err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error { | |||||
| var err error | |||||
| resp, err = c.subscriberClient.UpdateSnapshot(ctx, req, settings.GRPC...) | |||||
| return err | |||||
| }, opts...) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return resp, nil | |||||
| } | |||||
| // DeleteSnapshot removes an existing snapshot. Snapshots are used in | |||||
| // <a href="https://cloud.google.com/pubsub/docs/replay-overview">Seek</a> | |||||
| // operations, which allow | |||||
| // you to manage message acknowledgments in bulk. That is, you can set the | |||||
| // acknowledgment state of messages in an existing subscription to the state | |||||
| // captured by a snapshot.<br><br> | |||||
| // When the snapshot is deleted, all messages retained in the snapshot | |||||
| // are immediately dropped. After a snapshot is deleted, a new one may be | |||||
| // created with the same name, but the new one has no association with the old | |||||
| // snapshot or its subscription, unless the same subscription is specified. | |||||
| func (c *SubscriberClient) DeleteSnapshot(ctx context.Context, req *pubsubpb.DeleteSnapshotRequest, opts ...gax.CallOption) error { | |||||
| md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "snapshot", url.QueryEscape(req.GetSnapshot()))) | |||||
| ctx = insertMetadata(ctx, c.xGoogMetadata, md) | |||||
| opts = append(c.CallOptions.DeleteSnapshot[0:len(c.CallOptions.DeleteSnapshot):len(c.CallOptions.DeleteSnapshot)], opts...) | |||||
| err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error { | |||||
| var err error | |||||
| _, err = c.subscriberClient.DeleteSnapshot(ctx, req, settings.GRPC...) | |||||
| return err | |||||
| }, opts...) | |||||
| return err | |||||
| } | |||||
| // Seek seeks an existing subscription to a point in time or to a given snapshot, | |||||
| // whichever is provided in the request. Snapshots are used in | |||||
| // <a href="https://cloud.google.com/pubsub/docs/replay-overview">Seek</a> | |||||
| // operations, which allow | |||||
| // you to manage message acknowledgments in bulk. That is, you can set the | |||||
| // acknowledgment state of messages in an existing subscription to the state | |||||
| // captured by a snapshot. Note that both the subscription and the snapshot | |||||
| // must be on the same topic. | |||||
| func (c *SubscriberClient) Seek(ctx context.Context, req *pubsubpb.SeekRequest, opts ...gax.CallOption) (*pubsubpb.SeekResponse, error) { | |||||
| md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "subscription", url.QueryEscape(req.GetSubscription()))) | |||||
| ctx = insertMetadata(ctx, c.xGoogMetadata, md) | |||||
| opts = append(c.CallOptions.Seek[0:len(c.CallOptions.Seek):len(c.CallOptions.Seek)], opts...) | |||||
| var resp *pubsubpb.SeekResponse | |||||
| err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error { | |||||
| var err error | |||||
| resp, err = c.subscriberClient.Seek(ctx, req, settings.GRPC...) | |||||
| return err | |||||
| }, opts...) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return resp, nil | |||||
| } | |||||
| // SnapshotIterator manages a stream of *pubsubpb.Snapshot. | |||||
| type SnapshotIterator struct { | |||||
| items []*pubsubpb.Snapshot | |||||
| pageInfo *iterator.PageInfo | |||||
| nextFunc func() error | |||||
| // InternalFetch is for use by the Google Cloud Libraries only. | |||||
| // It is not part of the stable interface of this package. | |||||
| // | |||||
| // InternalFetch returns results from a single call to the underlying RPC. | |||||
| // The number of results is no greater than pageSize. | |||||
| // If there are no more results, nextPageToken is empty and err is nil. | |||||
| InternalFetch func(pageSize int, pageToken string) (results []*pubsubpb.Snapshot, nextPageToken string, err error) | |||||
| } | |||||
| // PageInfo supports pagination. See the google.golang.org/api/iterator package for details. | |||||
| func (it *SnapshotIterator) PageInfo() *iterator.PageInfo { | |||||
| return it.pageInfo | |||||
| } | |||||
| // Next returns the next result. Its second return value is iterator.Done if there are no more | |||||
| // results. Once Next returns Done, all subsequent calls will return Done. | |||||
| func (it *SnapshotIterator) Next() (*pubsubpb.Snapshot, error) { | |||||
| var item *pubsubpb.Snapshot | |||||
| if err := it.nextFunc(); err != nil { | |||||
| return item, err | |||||
| } | |||||
| item = it.items[0] | |||||
| it.items = it.items[1:] | |||||
| return item, nil | |||||
| } | |||||
| func (it *SnapshotIterator) bufLen() int { | |||||
| return len(it.items) | |||||
| } | |||||
| func (it *SnapshotIterator) takeBuf() interface{} { | |||||
| b := it.items | |||||
| it.items = nil | |||||
| return b | |||||
| } | |||||
| // SubscriptionIterator manages a stream of *pubsubpb.Subscription. | |||||
| type SubscriptionIterator struct { | |||||
| items []*pubsubpb.Subscription | |||||
| pageInfo *iterator.PageInfo | |||||
| nextFunc func() error | |||||
| // InternalFetch is for use by the Google Cloud Libraries only. | |||||
| // It is not part of the stable interface of this package. | |||||
| // | |||||
| // InternalFetch returns results from a single call to the underlying RPC. | |||||
| // The number of results is no greater than pageSize. | |||||
| // If there are no more results, nextPageToken is empty and err is nil. | |||||
| InternalFetch func(pageSize int, pageToken string) (results []*pubsubpb.Subscription, nextPageToken string, err error) | |||||
| } | |||||
| // PageInfo supports pagination. See the google.golang.org/api/iterator package for details. | |||||
| func (it *SubscriptionIterator) PageInfo() *iterator.PageInfo { | |||||
| return it.pageInfo | |||||
| } | |||||
| // Next returns the next result. Its second return value is iterator.Done if there are no more | |||||
| // results. Once Next returns Done, all subsequent calls will return Done. | |||||
| func (it *SubscriptionIterator) Next() (*pubsubpb.Subscription, error) { | |||||
| var item *pubsubpb.Subscription | |||||
| if err := it.nextFunc(); err != nil { | |||||
| return item, err | |||||
| } | |||||
| item = it.items[0] | |||||
| it.items = it.items[1:] | |||||
| return item, nil | |||||
| } | |||||
| func (it *SubscriptionIterator) bufLen() int { | |||||
| return len(it.items) | |||||
| } | |||||
| func (it *SubscriptionIterator) takeBuf() interface{} { | |||||
| b := it.items | |||||
| it.items = nil | |||||
| return b | |||||
| } | |||||
| @@ -0,0 +1,72 @@ | |||||
| // Copyright 2018 Google LLC | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // http://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| // +build psdebug | |||||
| package pubsub | |||||
| import ( | |||||
| "sync" | |||||
| "time" | |||||
| ) | |||||
| var ( | |||||
| dmu sync.Mutex | |||||
| msgTraces = map[string][]Event{} | |||||
| ackIDToMsgID = map[string]string{} | |||||
| ) | |||||
| type Event struct { | |||||
| Desc string | |||||
| At time.Time | |||||
| } | |||||
| func MessageEvents(msgID string) []Event { | |||||
| dmu.Lock() | |||||
| defer dmu.Unlock() | |||||
| return msgTraces[msgID] | |||||
| } | |||||
| func addRecv(msgID, ackID string, t time.Time) { | |||||
| dmu.Lock() | |||||
| defer dmu.Unlock() | |||||
| ackIDToMsgID[ackID] = msgID | |||||
| addEvent(msgID, "recv", t) | |||||
| } | |||||
| func addAcks(ackIDs []string) { | |||||
| dmu.Lock() | |||||
| defer dmu.Unlock() | |||||
| now := time.Now() | |||||
| for _, id := range ackIDs { | |||||
| addEvent(ackIDToMsgID[id], "ack", now) | |||||
| } | |||||
| } | |||||
| func addModAcks(ackIDs []string, deadlineSecs int32) { | |||||
| dmu.Lock() | |||||
| defer dmu.Unlock() | |||||
| desc := "modack" | |||||
| if deadlineSecs == 0 { | |||||
| desc = "nack" | |||||
| } | |||||
| now := time.Now() | |||||
| for _, id := range ackIDs { | |||||
| addEvent(ackIDToMsgID[id], desc, now) | |||||
| } | |||||
| } | |||||
| func addEvent(msgID, desc string, t time.Time) { | |||||
| msgTraces[msgID] = append(msgTraces[msgID], Event{desc, t}) | |||||
| } | |||||
| @@ -0,0 +1,140 @@ | |||||
| // Copyright 2016 Google LLC | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // http://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| /* | |||||
| Package pubsub provides an easy way to publish and receive Google Cloud Pub/Sub | |||||
| messages, hiding the details of the underlying server RPCs. Google Cloud | |||||
| Pub/Sub is a many-to-many, asynchronous messaging system that decouples senders | |||||
| and receivers. | |||||
| More information about Google Cloud Pub/Sub is available at | |||||
| https://cloud.google.com/pubsub/docs | |||||
| See https://godoc.org/cloud.google.com/go for authentication, timeouts, | |||||
| connection pooling and similar aspects of this package. | |||||
| Publishing | |||||
| Google Cloud Pub/Sub messages are published to topics. Topics may be created | |||||
| using the pubsub package like so: | |||||
| topic, err := pubsubClient.CreateTopic(context.Background(), "topic-name") | |||||
| Messages may then be published to a topic: | |||||
| res := topic.Publish(ctx, &pubsub.Message{Data: []byte("payload")}) | |||||
| Publish queues the message for publishing and returns immediately. When enough | |||||
| messages have accumulated, or enough time has elapsed, the batch of messages is | |||||
| sent to the Pub/Sub service. | |||||
| Publish returns a PublishResult, which behaves like a future: its Get method | |||||
| blocks until the message has been sent to the service. | |||||
| The first time you call Publish on a topic, goroutines are started in the | |||||
| background. To clean up these goroutines, call Stop: | |||||
| topic.Stop() | |||||
| Receiving | |||||
| To receive messages published to a topic, clients create subscriptions | |||||
| to the topic. There may be more than one subscription per topic; each message | |||||
| that is published to the topic will be delivered to all of its subscriptions. | |||||
| Subsciptions may be created like so: | |||||
| sub, err := pubsubClient.CreateSubscription(context.Background(), "sub-name", | |||||
| pubsub.SubscriptionConfig{Topic: topic}) | |||||
| Messages are then consumed from a subscription via callback. | |||||
| err := sub.Receive(context.Background(), func(ctx context.Context, m *Message) { | |||||
| log.Printf("Got message: %s", m.Data) | |||||
| m.Ack() | |||||
| }) | |||||
| if err != nil { | |||||
| // Handle error. | |||||
| } | |||||
| The callback is invoked concurrently by multiple goroutines, maximizing | |||||
| throughput. To terminate a call to Receive, cancel its context. | |||||
| Once client code has processed the message, it must call Message.Ack or | |||||
| message.Nack, otherwise the message will eventually be redelivered. If the | |||||
| client cannot or doesn't want to process the message, it can call Message.Nack | |||||
| to speed redelivery. For more information and configuration options, see | |||||
| "Deadlines" below. | |||||
| Note: It is possible for Messages to be redelivered, even if Message.Ack has | |||||
| been called. Client code must be robust to multiple deliveries of messages. | |||||
| Note: This uses pubsub's streaming pull feature. This feature properties that | |||||
| may be surprising. Please take a look at https://cloud.google.com/pubsub/docs/pull#streamingpull | |||||
| for more details on how streaming pull behaves compared to the synchronous | |||||
| pull method. | |||||
| Deadlines | |||||
| The default pubsub deadlines are suitable for most use cases, but may be | |||||
| overridden. This section describes the tradeoffs that should be considered | |||||
| when overriding the defaults. | |||||
| Behind the scenes, each message returned by the Pub/Sub server has an | |||||
| associated lease, known as an "ACK deadline". Unless a message is | |||||
| acknowledged within the ACK deadline, or the client requests that | |||||
| the ACK deadline be extended, the message will become eligible for redelivery. | |||||
| As a convenience, the pubsub client will automatically extend deadlines until | |||||
| either: | |||||
| * Message.Ack or Message.Nack is called, or | |||||
| * The "MaxExtension" period elapses from the time the message is fetched from the server. | |||||
| ACK deadlines are extended periodically by the client. The initial ACK | |||||
| deadline given to messages is 10s. The period between extensions, as well as the | |||||
| length of the extension, automatically adjust depending on the time it takes to ack | |||||
| messages, up to 10m. This has the effect that subscribers that process messages | |||||
| quickly have their message ack deadlines extended for a short amount, whereas | |||||
| subscribers that process message slowly have their message ack deadlines extended | |||||
| for a large amount. The net effect is fewer RPCs sent from the client library. | |||||
| For example, consider a subscriber that takes 3 minutes to process each message. | |||||
| Since the library has already recorded several 3 minute "time to ack"s in a | |||||
| percentile distribution, future message extensions are sent with a value of 3 | |||||
| minutes, every 3 minutes. Suppose the application crashes 5 seconds after the | |||||
| library sends such an extension: the Pub/Sub server would wait the remaining | |||||
| 2m55s before re-sending the messages out to other subscribers. | |||||
| Please note that the client library does not use the subscription's AckDeadline | |||||
| by default. To enforce the subscription AckDeadline, set MaxExtension to the | |||||
| subscription's AckDeadline: | |||||
| cfg, err := sub.Config(ctx) | |||||
| if err != nil { | |||||
| // TODO: handle err | |||||
| } | |||||
| sub.ReceiveSettings.MaxExtension = cfg.AckDeadline | |||||
| Slow Message Processing | |||||
| For use cases where message processing exceeds 30 minutes, we recommend using | |||||
| the base client in a pull model, since long-lived streams are periodically killed | |||||
| by firewalls. See the example at https://godoc.org/cloud.google.com/go/pubsub/apiv1#example-SubscriberClient-Pull-LengthyClientProcessing | |||||
| */ | |||||
| package pubsub // import "cloud.google.com/go/pubsub" | |||||
| @@ -0,0 +1,122 @@ | |||||
| // Copyright 2017 Google LLC | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // http://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| package pubsub | |||||
| import ( | |||||
| "context" | |||||
| "sync/atomic" | |||||
| "golang.org/x/sync/semaphore" | |||||
| ) | |||||
| // flowController implements flow control for Subscription.Receive. | |||||
| type flowController struct { | |||||
| maxCount int | |||||
| maxSize int // max total size of messages | |||||
| semCount, semSize *semaphore.Weighted // enforces max number and size of messages | |||||
| // Number of calls to acquire - number of calls to release. This can go | |||||
| // negative if semCount == nil and a large acquire is followed by multiple | |||||
| // small releases. | |||||
| // Atomic. | |||||
| countRemaining int64 | |||||
| } | |||||
| // newFlowController creates a new flowController that ensures no more than | |||||
| // maxCount messages or maxSize bytes are outstanding at once. If maxCount or | |||||
| // maxSize is < 1, then an unlimited number of messages or bytes is permitted, | |||||
| // respectively. | |||||
| func newFlowController(maxCount, maxSize int) *flowController { | |||||
| fc := &flowController{ | |||||
| maxCount: maxCount, | |||||
| maxSize: maxSize, | |||||
| semCount: nil, | |||||
| semSize: nil, | |||||
| } | |||||
| if maxCount > 0 { | |||||
| fc.semCount = semaphore.NewWeighted(int64(maxCount)) | |||||
| } | |||||
| if maxSize > 0 { | |||||
| fc.semSize = semaphore.NewWeighted(int64(maxSize)) | |||||
| } | |||||
| return fc | |||||
| } | |||||
| // acquire blocks until one message of size bytes can proceed or ctx is done. | |||||
| // It returns nil in the first case, or ctx.Err() in the second. | |||||
| // | |||||
| // acquire allows large messages to proceed by treating a size greater than maxSize | |||||
| // as if it were equal to maxSize. | |||||
| func (f *flowController) acquire(ctx context.Context, size int) error { | |||||
| if f.semCount != nil { | |||||
| if err := f.semCount.Acquire(ctx, 1); err != nil { | |||||
| return err | |||||
| } | |||||
| } | |||||
| if f.semSize != nil { | |||||
| if err := f.semSize.Acquire(ctx, f.bound(size)); err != nil { | |||||
| if f.semCount != nil { | |||||
| f.semCount.Release(1) | |||||
| } | |||||
| return err | |||||
| } | |||||
| } | |||||
| atomic.AddInt64(&f.countRemaining, 1) | |||||
| return nil | |||||
| } | |||||
| // tryAcquire returns false if acquire would block. Otherwise, it behaves like | |||||
| // acquire and returns true. | |||||
| // | |||||
| // tryAcquire allows large messages to proceed by treating a size greater than | |||||
| // maxSize as if it were equal to maxSize. | |||||
| func (f *flowController) tryAcquire(size int) bool { | |||||
| if f.semCount != nil { | |||||
| if !f.semCount.TryAcquire(1) { | |||||
| return false | |||||
| } | |||||
| } | |||||
| if f.semSize != nil { | |||||
| if !f.semSize.TryAcquire(f.bound(size)) { | |||||
| if f.semCount != nil { | |||||
| f.semCount.Release(1) | |||||
| } | |||||
| return false | |||||
| } | |||||
| } | |||||
| atomic.AddInt64(&f.countRemaining, 1) | |||||
| return true | |||||
| } | |||||
| // release notes that one message of size bytes is no longer outstanding. | |||||
| func (f *flowController) release(size int) { | |||||
| atomic.AddInt64(&f.countRemaining, -1) | |||||
| if f.semCount != nil { | |||||
| f.semCount.Release(1) | |||||
| } | |||||
| if f.semSize != nil { | |||||
| f.semSize.Release(f.bound(size)) | |||||
| } | |||||
| } | |||||
| func (f *flowController) bound(size int) int64 { | |||||
| if size > f.maxSize { | |||||
| return int64(f.maxSize) | |||||
| } | |||||
| return int64(size) | |||||
| } | |||||
| func (f *flowController) count() int { | |||||
| return int(atomic.LoadInt64(&f.countRemaining)) | |||||
| } | |||||
| @@ -0,0 +1,79 @@ | |||||
| // Copyright 2017 Google LLC | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // http://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| package distribution | |||||
| import ( | |||||
| "log" | |||||
| "math" | |||||
| "sort" | |||||
| "sync" | |||||
| "sync/atomic" | |||||
| ) | |||||
| // D is a distribution. Methods of D can be called concurrently by multiple | |||||
| // goroutines. | |||||
| type D struct { | |||||
| buckets []uint64 | |||||
| // sumsReuse is the scratch space that is reused | |||||
| // to store sums during invocations of Percentile. | |||||
| // After an invocation of New(n): | |||||
| // len(buckets) == len(sumsReuse) == n | |||||
| sumsReuse []uint64 | |||||
| mu sync.Mutex | |||||
| } | |||||
| // New creates a new distribution capable of holding values from 0 to n-1. | |||||
| func New(n int) *D { | |||||
| return &D{ | |||||
| buckets: make([]uint64, n), | |||||
| sumsReuse: make([]uint64, n), | |||||
| } | |||||
| } | |||||
| // Record records value v to the distribution. | |||||
| // To help with distributions with long tails, if v is larger than the maximum value, | |||||
| // Record records the maximum value instead. | |||||
| // If v is negative, Record panics. | |||||
| func (d *D) Record(v int) { | |||||
| if v < 0 { | |||||
| log.Panicf("Record: value out of range: %d", v) | |||||
| } else if v >= len(d.buckets) { | |||||
| v = len(d.buckets) - 1 | |||||
| } | |||||
| atomic.AddUint64(&d.buckets[v], 1) | |||||
| } | |||||
| // Percentile computes the p-th percentile of the distribution where | |||||
| // p is between 0 and 1. This method may be called by multiple goroutines. | |||||
| func (d *D) Percentile(p float64) int { | |||||
| // NOTE: This implementation uses the nearest-rank method. | |||||
| // https://en.wikipedia.org/wiki/Percentile#The_nearest-rank_method | |||||
| if p < 0 || p > 1 { | |||||
| log.Panicf("Percentile: percentile out of range: %f", p) | |||||
| } | |||||
| d.mu.Lock() | |||||
| defer d.mu.Unlock() | |||||
| var sum uint64 | |||||
| for i := range d.sumsReuse { | |||||
| sum += atomic.LoadUint64(&d.buckets[i]) | |||||
| d.sumsReuse[i] = sum | |||||
| } | |||||
| target := uint64(math.Ceil(float64(sum) * p)) | |||||
| return sort.Search(len(d.sumsReuse), func(i int) bool { return d.sumsReuse[i] >= target }) | |||||
| } | |||||
| @@ -0,0 +1,527 @@ | |||||
| // Copyright 2016 Google LLC | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // http://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| package pubsub | |||||
| import ( | |||||
| "context" | |||||
| "io" | |||||
| "sync" | |||||
| "time" | |||||
| vkit "cloud.google.com/go/pubsub/apiv1" | |||||
| "cloud.google.com/go/pubsub/internal/distribution" | |||||
| "github.com/golang/protobuf/proto" | |||||
| gax "github.com/googleapis/gax-go/v2" | |||||
| pb "google.golang.org/genproto/googleapis/pubsub/v1" | |||||
| "google.golang.org/grpc" | |||||
| "google.golang.org/grpc/codes" | |||||
| "google.golang.org/grpc/status" | |||||
| ) | |||||
| // Between message receipt and ack (that is, the time spent processing a message) we want to extend the message | |||||
| // deadline by way of modack. However, we don't want to extend the deadline right as soon as the deadline expires; | |||||
| // instead, we'd want to extend the deadline a little bit of time ahead. gracePeriod is that amount of time ahead | |||||
| // of the actual deadline. | |||||
| const gracePeriod = 5 * time.Second | |||||
| type messageIterator struct { | |||||
| ctx context.Context | |||||
| cancel func() // the function that will cancel ctx; called in stop | |||||
| po *pullOptions | |||||
| ps *pullStream | |||||
| subc *vkit.SubscriberClient | |||||
| subName string | |||||
| kaTick <-chan time.Time // keep-alive (deadline extensions) | |||||
| ackTicker *time.Ticker // message acks | |||||
| nackTicker *time.Ticker // message nacks (more frequent than acks) | |||||
| pingTicker *time.Ticker // sends to the stream to keep it open | |||||
| failed chan struct{} // closed on stream error | |||||
| drained chan struct{} // closed when stopped && no more pending messages | |||||
| wg sync.WaitGroup | |||||
| mu sync.Mutex | |||||
| ackTimeDist *distribution.D // dist uses seconds | |||||
| // keepAliveDeadlines is a map of id to expiration time. This map is used in conjunction with | |||||
| // subscription.ReceiveSettings.MaxExtension to record the maximum amount of time (the | |||||
| // deadline, more specifically) we're willing to extend a message's ack deadline. As each | |||||
| // message arrives, we'll record now+MaxExtension in this table; whenever we have a chance | |||||
| // to update ack deadlines (via modack), we'll consult this table and only include IDs | |||||
| // that are not beyond their deadline. | |||||
| keepAliveDeadlines map[string]time.Time | |||||
| pendingAcks map[string]bool | |||||
| pendingNacks map[string]bool | |||||
| pendingModAcks map[string]bool // ack IDs whose ack deadline is to be modified | |||||
| err error // error from stream failure | |||||
| } | |||||
| // newMessageIterator starts and returns a new messageIterator. | |||||
| // subName is the full name of the subscription to pull messages from. | |||||
| // Stop must be called on the messageIterator when it is no longer needed. | |||||
| // The iterator always uses the background context for acking messages and extending message deadlines. | |||||
| func newMessageIterator(subc *vkit.SubscriberClient, subName string, po *pullOptions) *messageIterator { | |||||
| var ps *pullStream | |||||
| if !po.synchronous { | |||||
| ps = newPullStream(context.Background(), subc.StreamingPull, subName) | |||||
| } | |||||
| // The period will update each tick based on the distribution of acks. We'll start by arbitrarily sending | |||||
| // the first keepAlive halfway towards the minimum ack deadline. | |||||
| keepAlivePeriod := minAckDeadline / 2 | |||||
| // Ack promptly so users don't lose work if client crashes. | |||||
| ackTicker := time.NewTicker(100 * time.Millisecond) | |||||
| nackTicker := time.NewTicker(100 * time.Millisecond) | |||||
| pingTicker := time.NewTicker(30 * time.Second) | |||||
| cctx, cancel := context.WithCancel(context.Background()) | |||||
| it := &messageIterator{ | |||||
| ctx: cctx, | |||||
| cancel: cancel, | |||||
| ps: ps, | |||||
| po: po, | |||||
| subc: subc, | |||||
| subName: subName, | |||||
| kaTick: time.After(keepAlivePeriod), | |||||
| ackTicker: ackTicker, | |||||
| nackTicker: nackTicker, | |||||
| pingTicker: pingTicker, | |||||
| failed: make(chan struct{}), | |||||
| drained: make(chan struct{}), | |||||
| ackTimeDist: distribution.New(int(maxAckDeadline/time.Second) + 1), | |||||
| keepAliveDeadlines: map[string]time.Time{}, | |||||
| pendingAcks: map[string]bool{}, | |||||
| pendingNacks: map[string]bool{}, | |||||
| pendingModAcks: map[string]bool{}, | |||||
| } | |||||
| it.wg.Add(1) | |||||
| go it.sender() | |||||
| return it | |||||
| } | |||||
| // Subscription.receive will call stop on its messageIterator when finished with it. | |||||
| // Stop will block until Done has been called on all Messages that have been | |||||
| // returned by Next, or until the context with which the messageIterator was created | |||||
| // is cancelled or exceeds its deadline. | |||||
| func (it *messageIterator) stop() { | |||||
| it.cancel() | |||||
| it.mu.Lock() | |||||
| it.checkDrained() | |||||
| it.mu.Unlock() | |||||
| it.wg.Wait() | |||||
| } | |||||
| // checkDrained closes the drained channel if the iterator has been stopped and all | |||||
| // pending messages have either been n/acked or expired. | |||||
| // | |||||
| // Called with the lock held. | |||||
| func (it *messageIterator) checkDrained() { | |||||
| select { | |||||
| case <-it.drained: | |||||
| return | |||||
| default: | |||||
| } | |||||
| select { | |||||
| case <-it.ctx.Done(): | |||||
| if len(it.keepAliveDeadlines) == 0 { | |||||
| close(it.drained) | |||||
| } | |||||
| default: | |||||
| } | |||||
| } | |||||
| // Called when a message is acked/nacked. | |||||
| func (it *messageIterator) done(ackID string, ack bool, receiveTime time.Time) { | |||||
| it.ackTimeDist.Record(int(time.Since(receiveTime) / time.Second)) | |||||
| it.mu.Lock() | |||||
| defer it.mu.Unlock() | |||||
| delete(it.keepAliveDeadlines, ackID) | |||||
| if ack { | |||||
| it.pendingAcks[ackID] = true | |||||
| } else { | |||||
| it.pendingNacks[ackID] = true | |||||
| } | |||||
| it.checkDrained() | |||||
| } | |||||
| // fail is called when a stream method returns a permanent error. | |||||
| // fail returns it.err. This may be err, or it may be the error | |||||
| // set by an earlier call to fail. | |||||
| func (it *messageIterator) fail(err error) error { | |||||
| it.mu.Lock() | |||||
| defer it.mu.Unlock() | |||||
| if it.err == nil { | |||||
| it.err = err | |||||
| close(it.failed) | |||||
| } | |||||
| return it.err | |||||
| } | |||||
| // receive makes a call to the stream's Recv method, or the Pull RPC, and returns | |||||
| // its messages. | |||||
| // maxToPull is the maximum number of messages for the Pull RPC. | |||||
| func (it *messageIterator) receive(maxToPull int32) ([]*Message, error) { | |||||
| it.mu.Lock() | |||||
| ierr := it.err | |||||
| it.mu.Unlock() | |||||
| if ierr != nil { | |||||
| return nil, ierr | |||||
| } | |||||
| // Stop retrieving messages if the iterator's Stop method was called. | |||||
| select { | |||||
| case <-it.ctx.Done(): | |||||
| it.wg.Wait() | |||||
| return nil, io.EOF | |||||
| default: | |||||
| } | |||||
| var rmsgs []*pb.ReceivedMessage | |||||
| var err error | |||||
| if it.po.synchronous { | |||||
| rmsgs, err = it.pullMessages(maxToPull) | |||||
| } else { | |||||
| rmsgs, err = it.recvMessages() | |||||
| } | |||||
| // Any error here is fatal. | |||||
| if err != nil { | |||||
| return nil, it.fail(err) | |||||
| } | |||||
| msgs, err := convertMessages(rmsgs) | |||||
| if err != nil { | |||||
| return nil, it.fail(err) | |||||
| } | |||||
| // We received some messages. Remember them so we can keep them alive. Also, | |||||
| // do a receipt mod-ack when streaming. | |||||
| maxExt := time.Now().Add(it.po.maxExtension) | |||||
| ackIDs := map[string]bool{} | |||||
| it.mu.Lock() | |||||
| now := time.Now() | |||||
| for _, m := range msgs { | |||||
| m.receiveTime = now | |||||
| addRecv(m.ID, m.ackID, now) | |||||
| m.doneFunc = it.done | |||||
| it.keepAliveDeadlines[m.ackID] = maxExt | |||||
| // Don't change the mod-ack if the message is going to be nacked. This is | |||||
| // possible if there are retries. | |||||
| if !it.pendingNacks[m.ackID] { | |||||
| ackIDs[m.ackID] = true | |||||
| } | |||||
| } | |||||
| deadline := it.ackDeadline() | |||||
| it.mu.Unlock() | |||||
| if len(ackIDs) > 0 { | |||||
| if !it.sendModAck(ackIDs, deadline) { | |||||
| return nil, it.err | |||||
| } | |||||
| } | |||||
| return msgs, nil | |||||
| } | |||||
| // Get messages using the Pull RPC. | |||||
| // This may block indefinitely. It may also return zero messages, after some time waiting. | |||||
| func (it *messageIterator) pullMessages(maxToPull int32) ([]*pb.ReceivedMessage, error) { | |||||
| // Use it.ctx as the RPC context, so that if the iterator is stopped, the call | |||||
| // will return immediately. | |||||
| res, err := it.subc.Pull(it.ctx, &pb.PullRequest{ | |||||
| Subscription: it.subName, | |||||
| MaxMessages: maxToPull, | |||||
| }, gax.WithGRPCOptions(grpc.MaxCallRecvMsgSize(maxSendRecvBytes))) | |||||
| switch { | |||||
| case err == context.Canceled: | |||||
| return nil, nil | |||||
| case err != nil: | |||||
| return nil, err | |||||
| default: | |||||
| return res.ReceivedMessages, nil | |||||
| } | |||||
| } | |||||
| func (it *messageIterator) recvMessages() ([]*pb.ReceivedMessage, error) { | |||||
| res, err := it.ps.Recv() | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return res.ReceivedMessages, nil | |||||
| } | |||||
| // sender runs in a goroutine and handles all sends to the stream. | |||||
| func (it *messageIterator) sender() { | |||||
| defer it.wg.Done() | |||||
| defer it.ackTicker.Stop() | |||||
| defer it.nackTicker.Stop() | |||||
| defer it.pingTicker.Stop() | |||||
| defer func() { | |||||
| if it.ps != nil { | |||||
| it.ps.CloseSend() | |||||
| } | |||||
| }() | |||||
| done := false | |||||
| for !done { | |||||
| sendAcks := false | |||||
| sendNacks := false | |||||
| sendModAcks := false | |||||
| sendPing := false | |||||
| dl := it.ackDeadline() | |||||
| select { | |||||
| case <-it.failed: | |||||
| // Stream failed: nothing to do, so stop immediately. | |||||
| return | |||||
| case <-it.drained: | |||||
| // All outstanding messages have been marked done: | |||||
| // nothing left to do except make the final calls. | |||||
| it.mu.Lock() | |||||
| sendAcks = (len(it.pendingAcks) > 0) | |||||
| sendNacks = (len(it.pendingNacks) > 0) | |||||
| // No point in sending modacks. | |||||
| done = true | |||||
| case <-it.kaTick: | |||||
| it.mu.Lock() | |||||
| it.handleKeepAlives() | |||||
| sendModAcks = (len(it.pendingModAcks) > 0) | |||||
| nextTick := dl - gracePeriod | |||||
| if nextTick <= 0 { | |||||
| // If the deadline is <= gracePeriod, let's tick again halfway to | |||||
| // the deadline. | |||||
| nextTick = dl / 2 | |||||
| } | |||||
| it.kaTick = time.After(nextTick) | |||||
| case <-it.nackTicker.C: | |||||
| it.mu.Lock() | |||||
| sendNacks = (len(it.pendingNacks) > 0) | |||||
| case <-it.ackTicker.C: | |||||
| it.mu.Lock() | |||||
| sendAcks = (len(it.pendingAcks) > 0) | |||||
| case <-it.pingTicker.C: | |||||
| it.mu.Lock() | |||||
| // Ping only if we are processing messages via streaming. | |||||
| sendPing = !it.po.synchronous && (len(it.keepAliveDeadlines) > 0) | |||||
| } | |||||
| // Lock is held here. | |||||
| var acks, nacks, modAcks map[string]bool | |||||
| if sendAcks { | |||||
| acks = it.pendingAcks | |||||
| it.pendingAcks = map[string]bool{} | |||||
| } | |||||
| if sendNacks { | |||||
| nacks = it.pendingNacks | |||||
| it.pendingNacks = map[string]bool{} | |||||
| } | |||||
| if sendModAcks { | |||||
| modAcks = it.pendingModAcks | |||||
| it.pendingModAcks = map[string]bool{} | |||||
| } | |||||
| it.mu.Unlock() | |||||
| // Make Ack and ModAck RPCs. | |||||
| if sendAcks { | |||||
| if !it.sendAck(acks) { | |||||
| return | |||||
| } | |||||
| } | |||||
| if sendNacks { | |||||
| // Nack indicated by modifying the deadline to zero. | |||||
| if !it.sendModAck(nacks, 0) { | |||||
| return | |||||
| } | |||||
| } | |||||
| if sendModAcks { | |||||
| if !it.sendModAck(modAcks, dl) { | |||||
| return | |||||
| } | |||||
| } | |||||
| if sendPing { | |||||
| it.pingStream() | |||||
| } | |||||
| } | |||||
| } | |||||
| // handleKeepAlives modifies the pending request to include deadline extensions | |||||
| // for live messages. It also purges expired messages. | |||||
| // | |||||
| // Called with the lock held. | |||||
| func (it *messageIterator) handleKeepAlives() { | |||||
| now := time.Now() | |||||
| for id, expiry := range it.keepAliveDeadlines { | |||||
| if expiry.Before(now) { | |||||
| // This delete will not result in skipping any map items, as implied by | |||||
| // the spec at https://golang.org/ref/spec#For_statements, "For | |||||
| // statements with range clause", note 3, and stated explicitly at | |||||
| // https://groups.google.com/forum/#!msg/golang-nuts/UciASUb03Js/pzSq5iVFAQAJ. | |||||
| delete(it.keepAliveDeadlines, id) | |||||
| } else { | |||||
| // This will not conflict with a nack, because nacking removes the ID from keepAliveDeadlines. | |||||
| it.pendingModAcks[id] = true | |||||
| } | |||||
| } | |||||
| it.checkDrained() | |||||
| } | |||||
| func (it *messageIterator) sendAck(m map[string]bool) bool { | |||||
| // Account for the Subscription field. | |||||
| overhead := calcFieldSizeString(it.subName) | |||||
| return it.sendAckIDRPC(m, maxPayload-overhead, func(ids []string) error { | |||||
| recordStat(it.ctx, AckCount, int64(len(ids))) | |||||
| addAcks(ids) | |||||
| // Use context.Background() as the call's context, not it.ctx. We don't | |||||
| // want to cancel this RPC when the iterator is stopped. | |||||
| return it.subc.Acknowledge(context.Background(), &pb.AcknowledgeRequest{ | |||||
| Subscription: it.subName, | |||||
| AckIds: ids, | |||||
| }) | |||||
| }) | |||||
| } | |||||
| // The receipt mod-ack amount is derived from a percentile distribution based | |||||
| // on the time it takes to process messages. The percentile chosen is the 99%th | |||||
| // percentile in order to capture the highest amount of time necessary without | |||||
| // considering 1% outliers. | |||||
| func (it *messageIterator) sendModAck(m map[string]bool, deadline time.Duration) bool { | |||||
| deadlineSec := int32(deadline / time.Second) | |||||
| // Account for the Subscription and AckDeadlineSeconds fields. | |||||
| overhead := calcFieldSizeString(it.subName) + calcFieldSizeInt(int(deadlineSec)) | |||||
| return it.sendAckIDRPC(m, maxPayload-overhead, func(ids []string) error { | |||||
| if deadline == 0 { | |||||
| recordStat(it.ctx, NackCount, int64(len(ids))) | |||||
| } else { | |||||
| recordStat(it.ctx, ModAckCount, int64(len(ids))) | |||||
| } | |||||
| addModAcks(ids, deadlineSec) | |||||
| // Retry this RPC on Unavailable for a short amount of time, then give up | |||||
| // without returning a fatal error. The utility of this RPC is by nature | |||||
| // transient (since the deadline is relative to the current time) and it | |||||
| // isn't crucial for correctness (since expired messages will just be | |||||
| // resent). | |||||
| cctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) | |||||
| defer cancel() | |||||
| bo := gax.Backoff{ | |||||
| Initial: 100 * time.Millisecond, | |||||
| Max: time.Second, | |||||
| Multiplier: 2, | |||||
| } | |||||
| for { | |||||
| err := it.subc.ModifyAckDeadline(cctx, &pb.ModifyAckDeadlineRequest{ | |||||
| Subscription: it.subName, | |||||
| AckDeadlineSeconds: deadlineSec, | |||||
| AckIds: ids, | |||||
| }) | |||||
| switch status.Code(err) { | |||||
| case codes.Unavailable: | |||||
| if err := gax.Sleep(cctx, bo.Pause()); err == nil { | |||||
| continue | |||||
| } | |||||
| // Treat sleep timeout like RPC timeout. | |||||
| fallthrough | |||||
| case codes.DeadlineExceeded: | |||||
| // Timeout. Not a fatal error, but note that it happened. | |||||
| recordStat(it.ctx, ModAckTimeoutCount, 1) | |||||
| return nil | |||||
| default: | |||||
| // Any other error is fatal. | |||||
| return err | |||||
| } | |||||
| } | |||||
| }) | |||||
| } | |||||
| func (it *messageIterator) sendAckIDRPC(ackIDSet map[string]bool, maxSize int, call func([]string) error) bool { | |||||
| ackIDs := make([]string, 0, len(ackIDSet)) | |||||
| for k := range ackIDSet { | |||||
| ackIDs = append(ackIDs, k) | |||||
| } | |||||
| var toSend []string | |||||
| for len(ackIDs) > 0 { | |||||
| toSend, ackIDs = splitRequestIDs(ackIDs, maxSize) | |||||
| if err := call(toSend); err != nil { | |||||
| // The underlying client handles retries, so any error is fatal to the | |||||
| // iterator. | |||||
| it.fail(err) | |||||
| return false | |||||
| } | |||||
| } | |||||
| return true | |||||
| } | |||||
| // Send a message to the stream to keep it open. The stream will close if there's no | |||||
| // traffic on it for a while. By keeping it open, we delay the start of the | |||||
| // expiration timer on messages that are buffered by gRPC or elsewhere in the | |||||
| // network. This matters if it takes a long time to process messages relative to the | |||||
| // default ack deadline, and if the messages are small enough so that many can fit | |||||
| // into the buffer. | |||||
| func (it *messageIterator) pingStream() { | |||||
| // Ignore error; if the stream is broken, this doesn't matter anyway. | |||||
| _ = it.ps.Send(&pb.StreamingPullRequest{}) | |||||
| } | |||||
| // calcFieldSizeString returns the number of bytes string fields | |||||
| // will take up in an encoded proto message. | |||||
| func calcFieldSizeString(fields ...string) int { | |||||
| overhead := 0 | |||||
| for _, field := range fields { | |||||
| overhead += 1 + len(field) + proto.SizeVarint(uint64(len(field))) | |||||
| } | |||||
| return overhead | |||||
| } | |||||
| // calcFieldSizeInt returns the number of bytes int fields | |||||
| // will take up in an encoded proto message. | |||||
| func calcFieldSizeInt(fields ...int) int { | |||||
| overhead := 0 | |||||
| for _, field := range fields { | |||||
| overhead += 1 + proto.SizeVarint(uint64(field)) | |||||
| } | |||||
| return overhead | |||||
| } | |||||
| // splitRequestIDs takes a slice of ackIDs and returns two slices such that the first | |||||
| // ackID slice can be used in a request where the payload does not exceed maxSize. | |||||
| func splitRequestIDs(ids []string, maxSize int) (prefix, remainder []string) { | |||||
| size := 0 | |||||
| i := 0 | |||||
| // TODO(hongalex): Use binary search to find split index, since ackIDs are | |||||
| // fairly constant. | |||||
| for size < maxSize && i < len(ids) { | |||||
| size += calcFieldSizeString(ids[i]) | |||||
| i++ | |||||
| } | |||||
| if size > maxSize { | |||||
| i-- | |||||
| } | |||||
| return ids[:i], ids[i:] | |||||
| } | |||||
| // The deadline to ack is derived from a percentile distribution based | |||||
| // on the time it takes to process messages. The percentile chosen is the 99%th | |||||
| // percentile - that is, processing times up to the 99%th longest processing | |||||
| // times should be safe. The highest 1% may expire. This number was chosen | |||||
| // as a way to cover most users' usecases without losing the value of | |||||
| // expiration. | |||||
| func (it *messageIterator) ackDeadline() time.Duration { | |||||
| pt := time.Duration(it.ackTimeDist.Percentile(.99)) * time.Second | |||||
| if pt > maxAckDeadline { | |||||
| return maxAckDeadline | |||||
| } | |||||
| if pt < minAckDeadline { | |||||
| return minAckDeadline | |||||
| } | |||||
| return pt | |||||
| } | |||||
| @@ -0,0 +1,100 @@ | |||||
| // Copyright 2016 Google LLC | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // http://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| package pubsub | |||||
| import ( | |||||
| "time" | |||||
| "github.com/golang/protobuf/ptypes" | |||||
| pb "google.golang.org/genproto/googleapis/pubsub/v1" | |||||
| ) | |||||
| // Message represents a Pub/Sub message. | |||||
| type Message struct { | |||||
| // ID identifies this message. | |||||
| // This ID is assigned by the server and is populated for Messages obtained from a subscription. | |||||
| // This field is read-only. | |||||
| ID string | |||||
| // Data is the actual data in the message. | |||||
| Data []byte | |||||
| // Attributes represents the key-value pairs the current message | |||||
| // is labelled with. | |||||
| Attributes map[string]string | |||||
| // ackID is the identifier to acknowledge this message. | |||||
| ackID string | |||||
| // The time at which the message was published. | |||||
| // This is populated by the server for Messages obtained from a subscription. | |||||
| // This field is read-only. | |||||
| PublishTime time.Time | |||||
| // receiveTime is the time the message was received by the client. | |||||
| receiveTime time.Time | |||||
| // size is the approximate size of the message's data and attributes. | |||||
| size int | |||||
| calledDone bool | |||||
| // The done method of the iterator that created this Message. | |||||
| doneFunc func(string, bool, time.Time) | |||||
| } | |||||
| func toMessage(resp *pb.ReceivedMessage) (*Message, error) { | |||||
| if resp.Message == nil { | |||||
| return &Message{ackID: resp.AckId}, nil | |||||
| } | |||||
| pubTime, err := ptypes.Timestamp(resp.Message.PublishTime) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return &Message{ | |||||
| ackID: resp.AckId, | |||||
| Data: resp.Message.Data, | |||||
| Attributes: resp.Message.Attributes, | |||||
| ID: resp.Message.MessageId, | |||||
| PublishTime: pubTime, | |||||
| }, nil | |||||
| } | |||||
| // Ack indicates successful processing of a Message passed to the Subscriber.Receive callback. | |||||
| // It should not be called on any other Message value. | |||||
| // If message acknowledgement fails, the Message will be redelivered. | |||||
| // Client code must call Ack or Nack when finished for each received Message. | |||||
| // Calls to Ack or Nack have no effect after the first call. | |||||
| func (m *Message) Ack() { | |||||
| m.done(true) | |||||
| } | |||||
| // Nack indicates that the client will not or cannot process a Message passed to the Subscriber.Receive callback. | |||||
| // It should not be called on any other Message value. | |||||
| // Nack will result in the Message being redelivered more quickly than if it were allowed to expire. | |||||
| // Client code must call Ack or Nack when finished for each received Message. | |||||
| // Calls to Ack or Nack have no effect after the first call. | |||||
| func (m *Message) Nack() { | |||||
| m.done(false) | |||||
| } | |||||
| func (m *Message) done(ack bool) { | |||||
| if m.calledDone { | |||||
| return | |||||
| } | |||||
| m.calledDone = true | |||||
| m.doneFunc(m.ackID, ack, m.receiveTime) | |||||
| } | |||||
| @@ -0,0 +1,25 @@ | |||||
| // Copyright 2018 Google LLC | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // http://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| // +build !psdebug | |||||
| package pubsub | |||||
| import "time" | |||||
| func addRecv(string, string, time.Time) {} | |||||
| func addAcks([]string) {} | |||||
| func addModAcks([]string, int32) {} | |||||
| @@ -0,0 +1,108 @@ | |||||
| // Copyright 2014 Google LLC | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // http://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| package pubsub // import "cloud.google.com/go/pubsub" | |||||
| import ( | |||||
| "context" | |||||
| "fmt" | |||||
| "os" | |||||
| "runtime" | |||||
| "time" | |||||
| "cloud.google.com/go/internal/version" | |||||
| vkit "cloud.google.com/go/pubsub/apiv1" | |||||
| "google.golang.org/api/option" | |||||
| "google.golang.org/grpc" | |||||
| "google.golang.org/grpc/keepalive" | |||||
| ) | |||||
| const ( | |||||
| // ScopePubSub grants permissions to view and manage Pub/Sub | |||||
| // topics and subscriptions. | |||||
| ScopePubSub = "https://www.googleapis.com/auth/pubsub" | |||||
| // ScopeCloudPlatform grants permissions to view and manage your data | |||||
| // across Google Cloud Platform services. | |||||
| ScopeCloudPlatform = "https://www.googleapis.com/auth/cloud-platform" | |||||
| maxAckDeadline = 10 * time.Minute | |||||
| ) | |||||
| // Client is a Google Pub/Sub client scoped to a single project. | |||||
| // | |||||
| // Clients should be reused rather than being created as needed. | |||||
| // A Client may be shared by multiple goroutines. | |||||
| type Client struct { | |||||
| projectID string | |||||
| pubc *vkit.PublisherClient | |||||
| subc *vkit.SubscriberClient | |||||
| } | |||||
| // NewClient creates a new PubSub client. | |||||
| func NewClient(ctx context.Context, projectID string, opts ...option.ClientOption) (c *Client, err error) { | |||||
| var o []option.ClientOption | |||||
| // Environment variables for gcloud emulator: | |||||
| // https://cloud.google.com/sdk/gcloud/reference/beta/emulators/pubsub/ | |||||
| if addr := os.Getenv("PUBSUB_EMULATOR_HOST"); addr != "" { | |||||
| conn, err := grpc.Dial(addr, grpc.WithInsecure()) | |||||
| if err != nil { | |||||
| return nil, fmt.Errorf("grpc.Dial: %v", err) | |||||
| } | |||||
| o = []option.ClientOption{option.WithGRPCConn(conn)} | |||||
| } else { | |||||
| o = []option.ClientOption{ | |||||
| // Create multiple connections to increase throughput. | |||||
| option.WithGRPCConnectionPool(runtime.GOMAXPROCS(0)), | |||||
| option.WithGRPCDialOption(grpc.WithKeepaliveParams(keepalive.ClientParameters{ | |||||
| Time: 5 * time.Minute, | |||||
| })), | |||||
| } | |||||
| o = append(o, openCensusOptions()...) | |||||
| } | |||||
| o = append(o, opts...) | |||||
| pubc, err := vkit.NewPublisherClient(ctx, o...) | |||||
| if err != nil { | |||||
| return nil, fmt.Errorf("pubsub: %v", err) | |||||
| } | |||||
| subc, err := vkit.NewSubscriberClient(ctx, option.WithGRPCConn(pubc.Connection())) | |||||
| if err != nil { | |||||
| // Should never happen, since we are passing in the connection. | |||||
| // If it does, we cannot close, because the user may have passed in their | |||||
| // own connection originally. | |||||
| return nil, fmt.Errorf("pubsub: %v", err) | |||||
| } | |||||
| pubc.SetGoogleClientInfo("gccl", version.Repo) | |||||
| return &Client{ | |||||
| projectID: projectID, | |||||
| pubc: pubc, | |||||
| subc: subc, | |||||
| }, nil | |||||
| } | |||||
| // Close releases any resources held by the client, | |||||
| // such as memory and goroutines. | |||||
| // | |||||
| // If the client is available for the lifetime of the program, then Close need not be | |||||
| // called at exit. | |||||
| func (c *Client) Close() error { | |||||
| // Return the first error, because the first call closes the connection. | |||||
| err := c.pubc.Close() | |||||
| _ = c.subc.Close() | |||||
| return err | |||||
| } | |||||
| func (c *Client) fullyQualifiedProjectName() string { | |||||
| return fmt.Sprintf("projects/%s", c.projectID) | |||||
| } | |||||
| @@ -0,0 +1,192 @@ | |||||
| // Copyright 2018 Google LLC | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // http://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| package pubsub | |||||
| import ( | |||||
| "context" | |||||
| "io" | |||||
| "sync" | |||||
| "time" | |||||
| gax "github.com/googleapis/gax-go/v2" | |||||
| pb "google.golang.org/genproto/googleapis/pubsub/v1" | |||||
| "google.golang.org/grpc" | |||||
| ) | |||||
| // A pullStream supports the methods of a StreamingPullClient, but re-opens | |||||
| // the stream on a retryable error. | |||||
| type pullStream struct { | |||||
| ctx context.Context | |||||
| open func() (pb.Subscriber_StreamingPullClient, error) | |||||
| mu sync.Mutex | |||||
| spc *pb.Subscriber_StreamingPullClient | |||||
| err error // permanent error | |||||
| } | |||||
| // for testing | |||||
| type streamingPullFunc func(context.Context, ...gax.CallOption) (pb.Subscriber_StreamingPullClient, error) | |||||
| func newPullStream(ctx context.Context, streamingPull streamingPullFunc, subName string) *pullStream { | |||||
| ctx = withSubscriptionKey(ctx, subName) | |||||
| return &pullStream{ | |||||
| ctx: ctx, | |||||
| open: func() (pb.Subscriber_StreamingPullClient, error) { | |||||
| spc, err := streamingPull(ctx, gax.WithGRPCOptions(grpc.MaxCallRecvMsgSize(maxSendRecvBytes))) | |||||
| if err == nil { | |||||
| recordStat(ctx, StreamRequestCount, 1) | |||||
| err = spc.Send(&pb.StreamingPullRequest{ | |||||
| Subscription: subName, | |||||
| // We modack messages when we receive them, so this value doesn't matter too much. | |||||
| StreamAckDeadlineSeconds: 60, | |||||
| }) | |||||
| } | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return spc, nil | |||||
| }, | |||||
| } | |||||
| } | |||||
| // get returns either a valid *StreamingPullClient (SPC), or a permanent error. | |||||
| // If the argument is nil, this is the first call for an RPC, and the current | |||||
| // SPC will be returned (or a new one will be opened). Otherwise, this call is a | |||||
| // request to re-open the stream because of a retryable error, and the argument | |||||
| // is a pointer to the SPC that returned the error. | |||||
| func (s *pullStream) get(spc *pb.Subscriber_StreamingPullClient) (*pb.Subscriber_StreamingPullClient, error) { | |||||
| s.mu.Lock() | |||||
| defer s.mu.Unlock() | |||||
| // A stored error is permanent. | |||||
| if s.err != nil { | |||||
| return nil, s.err | |||||
| } | |||||
| // If the context is done, so are we. | |||||
| s.err = s.ctx.Err() | |||||
| if s.err != nil { | |||||
| return nil, s.err | |||||
| } | |||||
| // If the current and argument SPCs differ, return the current one. This subsumes two cases: | |||||
| // 1. We have an SPC and the caller is getting the stream for the first time. | |||||
| // 2. The caller wants to retry, but they have an older SPC; we've already retried. | |||||
| if spc != s.spc { | |||||
| return s.spc, nil | |||||
| } | |||||
| // Either this is the very first call on this stream (s.spc == nil), or we have a valid | |||||
| // retry request. Either way, open a new stream. | |||||
| // The lock is held here for a long time, but it doesn't matter because no callers could get | |||||
| // anything done anyway. | |||||
| s.spc = new(pb.Subscriber_StreamingPullClient) | |||||
| *s.spc, s.err = s.openWithRetry() // Any error from openWithRetry is permanent. | |||||
| return s.spc, s.err | |||||
| } | |||||
| func (s *pullStream) openWithRetry() (pb.Subscriber_StreamingPullClient, error) { | |||||
| r := defaultRetryer{} | |||||
| for { | |||||
| recordStat(s.ctx, StreamOpenCount, 1) | |||||
| spc, err := s.open() | |||||
| bo, shouldRetry := r.Retry(err) | |||||
| if err != nil && shouldRetry { | |||||
| recordStat(s.ctx, StreamRetryCount, 1) | |||||
| if err := gax.Sleep(s.ctx, bo); err != nil { | |||||
| return nil, err | |||||
| } | |||||
| continue | |||||
| } | |||||
| return spc, err | |||||
| } | |||||
| } | |||||
| func (s *pullStream) call(f func(pb.Subscriber_StreamingPullClient) error, opts ...gax.CallOption) error { | |||||
| var settings gax.CallSettings | |||||
| for _, opt := range opts { | |||||
| opt.Resolve(&settings) | |||||
| } | |||||
| var r gax.Retryer = &defaultRetryer{} | |||||
| if settings.Retry != nil { | |||||
| r = settings.Retry() | |||||
| } | |||||
| var ( | |||||
| spc *pb.Subscriber_StreamingPullClient | |||||
| err error | |||||
| ) | |||||
| for { | |||||
| spc, err = s.get(spc) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| start := time.Now() | |||||
| err = f(*spc) | |||||
| if err != nil { | |||||
| bo, shouldRetry := r.Retry(err) | |||||
| if shouldRetry { | |||||
| recordStat(s.ctx, StreamRetryCount, 1) | |||||
| if time.Since(start) < 30*time.Second { // don't sleep if we've been blocked for a while | |||||
| if err := gax.Sleep(s.ctx, bo); err != nil { | |||||
| return err | |||||
| } | |||||
| } | |||||
| continue | |||||
| } | |||||
| s.mu.Lock() | |||||
| s.err = err | |||||
| s.mu.Unlock() | |||||
| } | |||||
| return err | |||||
| } | |||||
| } | |||||
| func (s *pullStream) Send(req *pb.StreamingPullRequest) error { | |||||
| return s.call(func(spc pb.Subscriber_StreamingPullClient) error { | |||||
| recordStat(s.ctx, AckCount, int64(len(req.AckIds))) | |||||
| zeroes := 0 | |||||
| for _, mds := range req.ModifyDeadlineSeconds { | |||||
| if mds == 0 { | |||||
| zeroes++ | |||||
| } | |||||
| } | |||||
| recordStat(s.ctx, NackCount, int64(zeroes)) | |||||
| recordStat(s.ctx, ModAckCount, int64(len(req.ModifyDeadlineSeconds)-zeroes)) | |||||
| recordStat(s.ctx, StreamRequestCount, 1) | |||||
| return spc.Send(req) | |||||
| }) | |||||
| } | |||||
| func (s *pullStream) Recv() (*pb.StreamingPullResponse, error) { | |||||
| var res *pb.StreamingPullResponse | |||||
| err := s.call(func(spc pb.Subscriber_StreamingPullClient) error { | |||||
| var err error | |||||
| recordStat(s.ctx, StreamResponseCount, 1) | |||||
| res, err = spc.Recv() | |||||
| if err == nil { | |||||
| recordStat(s.ctx, PullCount, int64(len(res.ReceivedMessages))) | |||||
| } | |||||
| return err | |||||
| }, gax.WithRetry(func() gax.Retryer { return &streamingPullRetryer{defaultRetryer: &defaultRetryer{}} })) | |||||
| return res, err | |||||
| } | |||||
| func (s *pullStream) CloseSend() error { | |||||
| err := s.call(func(spc pb.Subscriber_StreamingPullClient) error { | |||||
| return spc.CloseSend() | |||||
| }) | |||||
| s.mu.Lock() | |||||
| s.err = io.EOF // should not be retried | |||||
| s.mu.Unlock() | |||||
| return err | |||||
| } | |||||
| @@ -0,0 +1,100 @@ | |||||
| // Copyright 2016 Google LLC | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // http://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| package pubsub | |||||
| import ( | |||||
| "fmt" | |||||
| "math" | |||||
| "strings" | |||||
| "time" | |||||
| gax "github.com/googleapis/gax-go/v2" | |||||
| pb "google.golang.org/genproto/googleapis/pubsub/v1" | |||||
| "google.golang.org/grpc/codes" | |||||
| "google.golang.org/grpc/status" | |||||
| ) | |||||
| // maxPayload is the maximum number of bytes to devote to the | |||||
| // encoded AcknowledgementRequest / ModifyAckDeadline proto message. | |||||
| // | |||||
| // With gRPC there is no way for the client to know the server's max message size (it is | |||||
| // configurable on the server). We know from experience that it | |||||
| // it 512K. | |||||
| const ( | |||||
| maxPayload = 512 * 1024 | |||||
| maxSendRecvBytes = 20 * 1024 * 1024 // 20M | |||||
| ) | |||||
| func convertMessages(rms []*pb.ReceivedMessage) ([]*Message, error) { | |||||
| msgs := make([]*Message, 0, len(rms)) | |||||
| for i, m := range rms { | |||||
| msg, err := toMessage(m) | |||||
| if err != nil { | |||||
| return nil, fmt.Errorf("pubsub: cannot decode the retrieved message at index: %d, message: %+v", i, m) | |||||
| } | |||||
| msgs = append(msgs, msg) | |||||
| } | |||||
| return msgs, nil | |||||
| } | |||||
| func trunc32(i int64) int32 { | |||||
| if i > math.MaxInt32 { | |||||
| i = math.MaxInt32 | |||||
| } | |||||
| return int32(i) | |||||
| } | |||||
| type defaultRetryer struct { | |||||
| bo gax.Backoff | |||||
| } | |||||
| // Logic originally from | |||||
| // https://github.com/GoogleCloudPlatform/google-cloud-java/blob/master/google-cloud-clients/google-cloud-pubsub/src/main/java/com/google/cloud/pubsub/v1/StatusUtil.java | |||||
| func (r *defaultRetryer) Retry(err error) (pause time.Duration, shouldRetry bool) { | |||||
| s, ok := status.FromError(err) | |||||
| if !ok { // includes io.EOF, normal stream close, which causes us to reopen | |||||
| return r.bo.Pause(), true | |||||
| } | |||||
| switch s.Code() { | |||||
| case codes.DeadlineExceeded, codes.Internal, codes.ResourceExhausted, codes.Aborted: | |||||
| return r.bo.Pause(), true | |||||
| case codes.Unavailable: | |||||
| c := strings.Contains(s.Message(), "Server shutdownNow invoked") | |||||
| if !c { | |||||
| return r.bo.Pause(), true | |||||
| } | |||||
| return 0, false | |||||
| default: | |||||
| return 0, false | |||||
| } | |||||
| } | |||||
| type streamingPullRetryer struct { | |||||
| defaultRetryer gax.Retryer | |||||
| } | |||||
| // Does not retry ResourceExhausted. See: https://github.com/GoogleCloudPlatform/google-cloud-go/issues/1166#issuecomment-443744705 | |||||
| func (r *streamingPullRetryer) Retry(err error) (pause time.Duration, shouldRetry bool) { | |||||
| s, ok := status.FromError(err) | |||||
| if !ok { // call defaultRetryer so that its backoff can be used | |||||
| return r.defaultRetryer.Retry(err) | |||||
| } | |||||
| switch s.Code() { | |||||
| case codes.ResourceExhausted: | |||||
| return 0, false | |||||
| default: | |||||
| return r.defaultRetryer.Retry(err) | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,160 @@ | |||||
| // Copyright 2017 Google LLC | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // http://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| package pubsub | |||||
| import ( | |||||
| "context" | |||||
| "fmt" | |||||
| "strings" | |||||
| "time" | |||||
| "github.com/golang/protobuf/ptypes" | |||||
| pb "google.golang.org/genproto/googleapis/pubsub/v1" | |||||
| ) | |||||
| // Snapshot is a reference to a PubSub snapshot. | |||||
| type Snapshot struct { | |||||
| c *Client | |||||
| // The fully qualified identifier for the snapshot, in the format "projects/<projid>/snapshots/<snap>" | |||||
| name string | |||||
| } | |||||
| // ID returns the unique identifier of the snapshot within its project. | |||||
| func (s *Snapshot) ID() string { | |||||
| slash := strings.LastIndex(s.name, "/") | |||||
| if slash == -1 { | |||||
| // name is not a fully-qualified name. | |||||
| panic("bad snapshot name") | |||||
| } | |||||
| return s.name[slash+1:] | |||||
| } | |||||
| // SnapshotConfig contains the details of a Snapshot. | |||||
| type SnapshotConfig struct { | |||||
| *Snapshot | |||||
| Topic *Topic | |||||
| Expiration time.Time | |||||
| } | |||||
| // Snapshot creates a reference to a snapshot. | |||||
| func (c *Client) Snapshot(id string) *Snapshot { | |||||
| return &Snapshot{ | |||||
| c: c, | |||||
| name: fmt.Sprintf("projects/%s/snapshots/%s", c.projectID, id), | |||||
| } | |||||
| } | |||||
| // Snapshots returns an iterator which returns snapshots for this project. | |||||
| func (c *Client) Snapshots(ctx context.Context) *SnapshotConfigIterator { | |||||
| it := c.subc.ListSnapshots(ctx, &pb.ListSnapshotsRequest{ | |||||
| Project: c.fullyQualifiedProjectName(), | |||||
| }) | |||||
| next := func() (*SnapshotConfig, error) { | |||||
| snap, err := it.Next() | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return toSnapshotConfig(snap, c) | |||||
| } | |||||
| return &SnapshotConfigIterator{next: next} | |||||
| } | |||||
| // SnapshotConfigIterator is an iterator that returns a series of snapshots. | |||||
| type SnapshotConfigIterator struct { | |||||
| next func() (*SnapshotConfig, error) | |||||
| } | |||||
| // Next returns the next SnapshotConfig. Its second return value is iterator.Done if there are no more results. | |||||
| // Once Next returns iterator.Done, all subsequent calls will return iterator.Done. | |||||
| func (snaps *SnapshotConfigIterator) Next() (*SnapshotConfig, error) { | |||||
| return snaps.next() | |||||
| } | |||||
| // Delete deletes a snapshot. | |||||
| func (s *Snapshot) Delete(ctx context.Context) error { | |||||
| return s.c.subc.DeleteSnapshot(ctx, &pb.DeleteSnapshotRequest{Snapshot: s.name}) | |||||
| } | |||||
| // SeekToTime seeks the subscription to a point in time. | |||||
| // | |||||
| // Messages retained in the subscription that were published before this | |||||
| // time are marked as acknowledged, and messages retained in the | |||||
| // subscription that were published after this time are marked as | |||||
| // unacknowledged. Note that this operation affects only those messages | |||||
| // retained in the subscription (configured by SnapshotConfig). For example, | |||||
| // if `time` corresponds to a point before the message retention | |||||
| // window (or to a point before the system's notion of the subscription | |||||
| // creation time), only retained messages will be marked as unacknowledged, | |||||
| // and already-expunged messages will not be restored. | |||||
| func (s *Subscription) SeekToTime(ctx context.Context, t time.Time) error { | |||||
| ts, err := ptypes.TimestampProto(t) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| _, err = s.c.subc.Seek(ctx, &pb.SeekRequest{ | |||||
| Subscription: s.name, | |||||
| Target: &pb.SeekRequest_Time{Time: ts}, | |||||
| }) | |||||
| return err | |||||
| } | |||||
| // CreateSnapshot creates a new snapshot from this subscription. | |||||
| // The snapshot will be for the topic this subscription is subscribed to. | |||||
| // If the name is empty string, a unique name is assigned. | |||||
| // | |||||
| // The created snapshot is guaranteed to retain: | |||||
| // (a) The existing backlog on the subscription. More precisely, this is | |||||
| // defined as the messages in the subscription's backlog that are | |||||
| // unacknowledged when Snapshot returns without error. | |||||
| // (b) Any messages published to the subscription's topic following | |||||
| // Snapshot returning without error. | |||||
| func (s *Subscription) CreateSnapshot(ctx context.Context, name string) (*SnapshotConfig, error) { | |||||
| if name != "" { | |||||
| name = fmt.Sprintf("projects/%s/snapshots/%s", strings.Split(s.name, "/")[1], name) | |||||
| } | |||||
| snap, err := s.c.subc.CreateSnapshot(ctx, &pb.CreateSnapshotRequest{ | |||||
| Name: name, | |||||
| Subscription: s.name, | |||||
| }) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return toSnapshotConfig(snap, s.c) | |||||
| } | |||||
| // SeekToSnapshot seeks the subscription to a snapshot. | |||||
| // | |||||
| // The snapshot need not be created from this subscription, | |||||
| // but it must be for the topic this subscription is subscribed to. | |||||
| func (s *Subscription) SeekToSnapshot(ctx context.Context, snap *Snapshot) error { | |||||
| _, err := s.c.subc.Seek(ctx, &pb.SeekRequest{ | |||||
| Subscription: s.name, | |||||
| Target: &pb.SeekRequest_Snapshot{Snapshot: snap.name}, | |||||
| }) | |||||
| return err | |||||
| } | |||||
| func toSnapshotConfig(snap *pb.Snapshot, c *Client) (*SnapshotConfig, error) { | |||||
| exp, err := ptypes.Timestamp(snap.ExpireTime) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return &SnapshotConfig{ | |||||
| Snapshot: &Snapshot{c: c, name: snap.Name}, | |||||
| Topic: newTopic(c, snap.Topic), | |||||
| Expiration: exp, | |||||
| }, nil | |||||
| } | |||||
| @@ -0,0 +1,741 @@ | |||||
| // Copyright 2016 Google LLC | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // http://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| package pubsub | |||||
| import ( | |||||
| "context" | |||||
| "errors" | |||||
| "fmt" | |||||
| "io" | |||||
| "strings" | |||||
| "sync" | |||||
| "time" | |||||
| "cloud.google.com/go/iam" | |||||
| "cloud.google.com/go/internal/optional" | |||||
| "github.com/golang/protobuf/ptypes" | |||||
| durpb "github.com/golang/protobuf/ptypes/duration" | |||||
| gax "github.com/googleapis/gax-go/v2" | |||||
| "golang.org/x/sync/errgroup" | |||||
| pb "google.golang.org/genproto/googleapis/pubsub/v1" | |||||
| fmpb "google.golang.org/genproto/protobuf/field_mask" | |||||
| "google.golang.org/grpc/codes" | |||||
| "google.golang.org/grpc/status" | |||||
| ) | |||||
| // Subscription is a reference to a PubSub subscription. | |||||
| type Subscription struct { | |||||
| c *Client | |||||
| // The fully qualified identifier for the subscription, in the format "projects/<projid>/subscriptions/<name>" | |||||
| name string | |||||
| // Settings for pulling messages. Configure these before calling Receive. | |||||
| ReceiveSettings ReceiveSettings | |||||
| mu sync.Mutex | |||||
| receiveActive bool | |||||
| } | |||||
| // Subscription creates a reference to a subscription. | |||||
| func (c *Client) Subscription(id string) *Subscription { | |||||
| return c.SubscriptionInProject(id, c.projectID) | |||||
| } | |||||
| // SubscriptionInProject creates a reference to a subscription in a given project. | |||||
| func (c *Client) SubscriptionInProject(id, projectID string) *Subscription { | |||||
| return &Subscription{ | |||||
| c: c, | |||||
| name: fmt.Sprintf("projects/%s/subscriptions/%s", projectID, id), | |||||
| } | |||||
| } | |||||
| // String returns the globally unique printable name of the subscription. | |||||
| func (s *Subscription) String() string { | |||||
| return s.name | |||||
| } | |||||
| // ID returns the unique identifier of the subscription within its project. | |||||
| func (s *Subscription) ID() string { | |||||
| slash := strings.LastIndex(s.name, "/") | |||||
| if slash == -1 { | |||||
| // name is not a fully-qualified name. | |||||
| panic("bad subscription name") | |||||
| } | |||||
| return s.name[slash+1:] | |||||
| } | |||||
| // Subscriptions returns an iterator which returns all of the subscriptions for the client's project. | |||||
| func (c *Client) Subscriptions(ctx context.Context) *SubscriptionIterator { | |||||
| it := c.subc.ListSubscriptions(ctx, &pb.ListSubscriptionsRequest{ | |||||
| Project: c.fullyQualifiedProjectName(), | |||||
| }) | |||||
| return &SubscriptionIterator{ | |||||
| c: c, | |||||
| next: func() (string, error) { | |||||
| sub, err := it.Next() | |||||
| if err != nil { | |||||
| return "", err | |||||
| } | |||||
| return sub.Name, nil | |||||
| }, | |||||
| } | |||||
| } | |||||
| // SubscriptionIterator is an iterator that returns a series of subscriptions. | |||||
| type SubscriptionIterator struct { | |||||
| c *Client | |||||
| next func() (string, error) | |||||
| } | |||||
| // Next returns the next subscription. If there are no more subscriptions, iterator.Done will be returned. | |||||
| func (subs *SubscriptionIterator) Next() (*Subscription, error) { | |||||
| subName, err := subs.next() | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return &Subscription{c: subs.c, name: subName}, nil | |||||
| } | |||||
| // PushConfig contains configuration for subscriptions that operate in push mode. | |||||
| type PushConfig struct { | |||||
| // A URL locating the endpoint to which messages should be pushed. | |||||
| Endpoint string | |||||
| // Endpoint configuration attributes. See https://cloud.google.com/pubsub/docs/reference/rest/v1/projects.subscriptions#pushconfig for more details. | |||||
| Attributes map[string]string | |||||
| // AuthenticationMethod is used by push endpoints to verify the source | |||||
| // of push requests. | |||||
| // It can be used with push endpoints that are private by default to | |||||
| // allow requests only from the Cloud Pub/Sub system, for example. | |||||
| // This field is optional and should be set only by users interested in | |||||
| // authenticated push. | |||||
| // | |||||
| // It is EXPERIMENTAL and a part of a closed alpha that may not be | |||||
| // accessible to all users. This field is subject to change or removal | |||||
| // without notice. | |||||
| AuthenticationMethod AuthenticationMethod | |||||
| } | |||||
| func (pc *PushConfig) toProto() *pb.PushConfig { | |||||
| if pc == nil { | |||||
| return nil | |||||
| } | |||||
| pbCfg := &pb.PushConfig{ | |||||
| Attributes: pc.Attributes, | |||||
| PushEndpoint: pc.Endpoint, | |||||
| } | |||||
| if authMethod := pc.AuthenticationMethod; authMethod != nil { | |||||
| switch am := authMethod.(type) { | |||||
| case *OIDCToken: | |||||
| pbCfg.AuthenticationMethod = am.toProto() | |||||
| default: // TODO: add others here when GAIC adds more definitions. | |||||
| } | |||||
| } | |||||
| return pbCfg | |||||
| } | |||||
| // AuthenticationMethod is used by push points to verify the source of push requests. | |||||
| // This interface defines fields that are part of a closed alpha that may not be accessible | |||||
| // to all users. | |||||
| type AuthenticationMethod interface { | |||||
| isAuthMethod() bool | |||||
| } | |||||
| // OIDCToken allows PushConfigs to be authenticated using | |||||
| // the OpenID Connect protocol https://openid.net/connect/ | |||||
| type OIDCToken struct { | |||||
| // Audience to be used when generating OIDC token. The audience claim | |||||
| // identifies the recipients that the JWT is intended for. The audience | |||||
| // value is a single case-sensitive string. Having multiple values (array) | |||||
| // for the audience field is not supported. More info about the OIDC JWT | |||||
| // token audience here: https://tools.ietf.org/html/rfc7519#section-4.1.3 | |||||
| // Note: if not specified, the Push endpoint URL will be used. | |||||
| Audience string | |||||
| // The service account email to be used for generating the OpenID Connect token. | |||||
| // The caller of: | |||||
| // * CreateSubscription | |||||
| // * UpdateSubscription | |||||
| // * ModifyPushConfig | |||||
| // calls must have the iam.serviceAccounts.actAs permission for the service account. | |||||
| // See https://cloud.google.com/iam/docs/understanding-roles#service-accounts-roles. | |||||
| ServiceAccountEmail string | |||||
| } | |||||
| var _ AuthenticationMethod = (*OIDCToken)(nil) | |||||
| func (oidcToken *OIDCToken) isAuthMethod() bool { return true } | |||||
| func (oidcToken *OIDCToken) toProto() *pb.PushConfig_OidcToken_ { | |||||
| if oidcToken == nil { | |||||
| return nil | |||||
| } | |||||
| return &pb.PushConfig_OidcToken_{ | |||||
| OidcToken: &pb.PushConfig_OidcToken{ | |||||
| Audience: oidcToken.Audience, | |||||
| ServiceAccountEmail: oidcToken.ServiceAccountEmail, | |||||
| }, | |||||
| } | |||||
| } | |||||
| // SubscriptionConfig describes the configuration of a subscription. | |||||
| type SubscriptionConfig struct { | |||||
| Topic *Topic | |||||
| PushConfig PushConfig | |||||
| // The default maximum time after a subscriber receives a message before | |||||
| // the subscriber should acknowledge the message. Note: messages which are | |||||
| // obtained via Subscription.Receive need not be acknowledged within this | |||||
| // deadline, as the deadline will be automatically extended. | |||||
| AckDeadline time.Duration | |||||
| // Whether to retain acknowledged messages. If true, acknowledged messages | |||||
| // will not be expunged until they fall out of the RetentionDuration window. | |||||
| RetainAckedMessages bool | |||||
| // How long to retain messages in backlog, from the time of publish. If | |||||
| // RetainAckedMessages is true, this duration affects the retention of | |||||
| // acknowledged messages, otherwise only unacknowledged messages are retained. | |||||
| // Defaults to 7 days. Cannot be longer than 7 days or shorter than 10 minutes. | |||||
| RetentionDuration time.Duration | |||||
| // Expiration policy specifies the conditions for a subscription's expiration. | |||||
| // A subscription is considered active as long as any connected subscriber is | |||||
| // successfully consuming messages from the subscription or is issuing | |||||
| // operations on the subscription. If `expiration_policy` is not set, a | |||||
| // *default policy* with `ttl` of 31 days will be used. The minimum allowed | |||||
| // value for `expiration_policy.ttl` is 1 day. | |||||
| // | |||||
| // Use time.Duration(0) to indicate that the subscription should never expire. | |||||
| // | |||||
| // It is EXPERIMENTAL and subject to change or removal without notice. | |||||
| ExpirationPolicy optional.Duration | |||||
| // The set of labels for the subscription. | |||||
| Labels map[string]string | |||||
| } | |||||
| func (cfg *SubscriptionConfig) toProto(name string) *pb.Subscription { | |||||
| var pbPushConfig *pb.PushConfig | |||||
| if cfg.PushConfig.Endpoint != "" || len(cfg.PushConfig.Attributes) != 0 || cfg.PushConfig.AuthenticationMethod != nil { | |||||
| pbPushConfig = cfg.PushConfig.toProto() | |||||
| } | |||||
| var retentionDuration *durpb.Duration | |||||
| if cfg.RetentionDuration != 0 { | |||||
| retentionDuration = ptypes.DurationProto(cfg.RetentionDuration) | |||||
| } | |||||
| return &pb.Subscription{ | |||||
| Name: name, | |||||
| Topic: cfg.Topic.name, | |||||
| PushConfig: pbPushConfig, | |||||
| AckDeadlineSeconds: trunc32(int64(cfg.AckDeadline.Seconds())), | |||||
| RetainAckedMessages: cfg.RetainAckedMessages, | |||||
| MessageRetentionDuration: retentionDuration, | |||||
| Labels: cfg.Labels, | |||||
| ExpirationPolicy: expirationPolicyToProto(cfg.ExpirationPolicy), | |||||
| } | |||||
| } | |||||
| func protoToSubscriptionConfig(pbSub *pb.Subscription, c *Client) (SubscriptionConfig, error) { | |||||
| rd := time.Hour * 24 * 7 | |||||
| var err error | |||||
| if pbSub.MessageRetentionDuration != nil { | |||||
| rd, err = ptypes.Duration(pbSub.MessageRetentionDuration) | |||||
| if err != nil { | |||||
| return SubscriptionConfig{}, err | |||||
| } | |||||
| } | |||||
| var expirationPolicy time.Duration | |||||
| if ttl := pbSub.ExpirationPolicy.GetTtl(); ttl != nil { | |||||
| expirationPolicy, err = ptypes.Duration(ttl) | |||||
| if err != nil { | |||||
| return SubscriptionConfig{}, err | |||||
| } | |||||
| } | |||||
| subC := SubscriptionConfig{ | |||||
| Topic: newTopic(c, pbSub.Topic), | |||||
| AckDeadline: time.Second * time.Duration(pbSub.AckDeadlineSeconds), | |||||
| RetainAckedMessages: pbSub.RetainAckedMessages, | |||||
| RetentionDuration: rd, | |||||
| Labels: pbSub.Labels, | |||||
| ExpirationPolicy: expirationPolicy, | |||||
| } | |||||
| pc := protoToPushConfig(pbSub.PushConfig) | |||||
| if pc != nil { | |||||
| subC.PushConfig = *pc | |||||
| } | |||||
| return subC, nil | |||||
| } | |||||
| func protoToPushConfig(pbPc *pb.PushConfig) *PushConfig { | |||||
| if pbPc == nil { | |||||
| return nil | |||||
| } | |||||
| pc := &PushConfig{ | |||||
| Endpoint: pbPc.PushEndpoint, | |||||
| Attributes: pbPc.Attributes, | |||||
| } | |||||
| if am := pbPc.AuthenticationMethod; am != nil { | |||||
| if oidcToken, ok := am.(*pb.PushConfig_OidcToken_); ok && oidcToken != nil && oidcToken.OidcToken != nil { | |||||
| pc.AuthenticationMethod = &OIDCToken{ | |||||
| Audience: oidcToken.OidcToken.GetAudience(), | |||||
| ServiceAccountEmail: oidcToken.OidcToken.GetServiceAccountEmail(), | |||||
| } | |||||
| } | |||||
| } | |||||
| return pc | |||||
| } | |||||
| // ReceiveSettings configure the Receive method. | |||||
| // A zero ReceiveSettings will result in values equivalent to DefaultReceiveSettings. | |||||
| type ReceiveSettings struct { | |||||
| // MaxExtension is the maximum period for which the Subscription should | |||||
| // automatically extend the ack deadline for each message. | |||||
| // | |||||
| // The Subscription will automatically extend the ack deadline of all | |||||
| // fetched Messages up to the duration specified. Automatic deadline | |||||
| // extension beyond the initial receipt may be disabled by specifying a | |||||
| // duration less than 0. | |||||
| MaxExtension time.Duration | |||||
| // MaxOutstandingMessages is the maximum number of unprocessed messages | |||||
| // (unacknowledged but not yet expired). If MaxOutstandingMessages is 0, it | |||||
| // will be treated as if it were DefaultReceiveSettings.MaxOutstandingMessages. | |||||
| // If the value is negative, then there will be no limit on the number of | |||||
| // unprocessed messages. | |||||
| MaxOutstandingMessages int | |||||
| // MaxOutstandingBytes is the maximum size of unprocessed messages | |||||
| // (unacknowledged but not yet expired). If MaxOutstandingBytes is 0, it will | |||||
| // be treated as if it were DefaultReceiveSettings.MaxOutstandingBytes. If | |||||
| // the value is negative, then there will be no limit on the number of bytes | |||||
| // for unprocessed messages. | |||||
| MaxOutstandingBytes int | |||||
| // NumGoroutines is the number of goroutines Receive will spawn to pull | |||||
| // messages concurrently. If NumGoroutines is less than 1, it will be treated | |||||
| // as if it were DefaultReceiveSettings.NumGoroutines. | |||||
| // | |||||
| // NumGoroutines does not limit the number of messages that can be processed | |||||
| // concurrently. Even with one goroutine, many messages might be processed at | |||||
| // once, because that goroutine may continually receive messages and invoke the | |||||
| // function passed to Receive on them. To limit the number of messages being | |||||
| // processed concurrently, set MaxOutstandingMessages. | |||||
| NumGoroutines int | |||||
| // If Synchronous is true, then no more than MaxOutstandingMessages will be in | |||||
| // memory at one time. (In contrast, when Synchronous is false, more than | |||||
| // MaxOutstandingMessages may have been received from the service and in memory | |||||
| // before being processed.) MaxOutstandingBytes still refers to the total bytes | |||||
| // processed, rather than in memory. NumGoroutines is ignored. | |||||
| // The default is false. | |||||
| Synchronous bool | |||||
| } | |||||
| // For synchronous receive, the time to wait if we are already processing | |||||
| // MaxOutstandingMessages. There is no point calling Pull and asking for zero | |||||
| // messages, so we pause to allow some message-processing callbacks to finish. | |||||
| // | |||||
| // The wait time is large enough to avoid consuming significant CPU, but | |||||
| // small enough to provide decent throughput. Users who want better | |||||
| // throughput should not be using synchronous mode. | |||||
| // | |||||
| // Waiting might seem like polling, so it's natural to think we could do better by | |||||
| // noticing when a callback is finished and immediately calling Pull. But if | |||||
| // callbacks finish in quick succession, this will result in frequent Pull RPCs that | |||||
| // request a single message, which wastes network bandwidth. Better to wait for a few | |||||
| // callbacks to finish, so we make fewer RPCs fetching more messages. | |||||
| // | |||||
| // This value is unexported so the user doesn't have another knob to think about. Note that | |||||
| // it is the same value as the one used for nackTicker, so it matches this client's | |||||
| // idea of a duration that is short, but not so short that we perform excessive RPCs. | |||||
| const synchronousWaitTime = 100 * time.Millisecond | |||||
| // This is a var so that tests can change it. | |||||
| var minAckDeadline = 10 * time.Second | |||||
| // DefaultReceiveSettings holds the default values for ReceiveSettings. | |||||
| var DefaultReceiveSettings = ReceiveSettings{ | |||||
| MaxExtension: 10 * time.Minute, | |||||
| MaxOutstandingMessages: 1000, | |||||
| MaxOutstandingBytes: 1e9, // 1G | |||||
| NumGoroutines: 1, | |||||
| } | |||||
| // Delete deletes the subscription. | |||||
| func (s *Subscription) Delete(ctx context.Context) error { | |||||
| return s.c.subc.DeleteSubscription(ctx, &pb.DeleteSubscriptionRequest{Subscription: s.name}) | |||||
| } | |||||
| // Exists reports whether the subscription exists on the server. | |||||
| func (s *Subscription) Exists(ctx context.Context) (bool, error) { | |||||
| _, err := s.c.subc.GetSubscription(ctx, &pb.GetSubscriptionRequest{Subscription: s.name}) | |||||
| if err == nil { | |||||
| return true, nil | |||||
| } | |||||
| if status.Code(err) == codes.NotFound { | |||||
| return false, nil | |||||
| } | |||||
| return false, err | |||||
| } | |||||
| // Config fetches the current configuration for the subscription. | |||||
| func (s *Subscription) Config(ctx context.Context) (SubscriptionConfig, error) { | |||||
| pbSub, err := s.c.subc.GetSubscription(ctx, &pb.GetSubscriptionRequest{Subscription: s.name}) | |||||
| if err != nil { | |||||
| return SubscriptionConfig{}, err | |||||
| } | |||||
| cfg, err := protoToSubscriptionConfig(pbSub, s.c) | |||||
| if err != nil { | |||||
| return SubscriptionConfig{}, err | |||||
| } | |||||
| return cfg, nil | |||||
| } | |||||
| // SubscriptionConfigToUpdate describes how to update a subscription. | |||||
| type SubscriptionConfigToUpdate struct { | |||||
| // If non-nil, the push config is changed. | |||||
| PushConfig *PushConfig | |||||
| // If non-zero, the ack deadline is changed. | |||||
| AckDeadline time.Duration | |||||
| // If set, RetainAckedMessages is changed. | |||||
| RetainAckedMessages optional.Bool | |||||
| // If non-zero, RetentionDuration is changed. | |||||
| RetentionDuration time.Duration | |||||
| // If non-zero, Expiration is changed. | |||||
| ExpirationPolicy optional.Duration | |||||
| // If non-nil, the current set of labels is completely | |||||
| // replaced by the new set. | |||||
| // This field has beta status. It is not subject to the stability guarantee | |||||
| // and may change. | |||||
| Labels map[string]string | |||||
| } | |||||
| // Update changes an existing subscription according to the fields set in cfg. | |||||
| // It returns the new SubscriptionConfig. | |||||
| // | |||||
| // Update returns an error if no fields were modified. | |||||
| func (s *Subscription) Update(ctx context.Context, cfg SubscriptionConfigToUpdate) (SubscriptionConfig, error) { | |||||
| req := s.updateRequest(&cfg) | |||||
| if err := cfg.validate(); err != nil { | |||||
| return SubscriptionConfig{}, fmt.Errorf("pubsub: UpdateSubscription %v", err) | |||||
| } | |||||
| if len(req.UpdateMask.Paths) == 0 { | |||||
| return SubscriptionConfig{}, errors.New("pubsub: UpdateSubscription call with nothing to update") | |||||
| } | |||||
| rpsub, err := s.c.subc.UpdateSubscription(ctx, req) | |||||
| if err != nil { | |||||
| return SubscriptionConfig{}, err | |||||
| } | |||||
| return protoToSubscriptionConfig(rpsub, s.c) | |||||
| } | |||||
| func (s *Subscription) updateRequest(cfg *SubscriptionConfigToUpdate) *pb.UpdateSubscriptionRequest { | |||||
| psub := &pb.Subscription{Name: s.name} | |||||
| var paths []string | |||||
| if cfg.PushConfig != nil { | |||||
| psub.PushConfig = cfg.PushConfig.toProto() | |||||
| paths = append(paths, "push_config") | |||||
| } | |||||
| if cfg.AckDeadline != 0 { | |||||
| psub.AckDeadlineSeconds = trunc32(int64(cfg.AckDeadline.Seconds())) | |||||
| paths = append(paths, "ack_deadline_seconds") | |||||
| } | |||||
| if cfg.RetainAckedMessages != nil { | |||||
| psub.RetainAckedMessages = optional.ToBool(cfg.RetainAckedMessages) | |||||
| paths = append(paths, "retain_acked_messages") | |||||
| } | |||||
| if cfg.RetentionDuration != 0 { | |||||
| psub.MessageRetentionDuration = ptypes.DurationProto(cfg.RetentionDuration) | |||||
| paths = append(paths, "message_retention_duration") | |||||
| } | |||||
| if cfg.ExpirationPolicy != nil { | |||||
| psub.ExpirationPolicy = expirationPolicyToProto(cfg.ExpirationPolicy) | |||||
| paths = append(paths, "expiration_policy") | |||||
| } | |||||
| if cfg.Labels != nil { | |||||
| psub.Labels = cfg.Labels | |||||
| paths = append(paths, "labels") | |||||
| } | |||||
| return &pb.UpdateSubscriptionRequest{ | |||||
| Subscription: psub, | |||||
| UpdateMask: &fmpb.FieldMask{Paths: paths}, | |||||
| } | |||||
| } | |||||
| const ( | |||||
| // The minimum expiration policy duration is 1 day as per: | |||||
| // https://github.com/googleapis/googleapis/blob/51145ff7812d2bb44c1219d0b76dac92a8bd94b2/google/pubsub/v1/pubsub.proto#L606-L607 | |||||
| minExpirationPolicy = 24 * time.Hour | |||||
| // If an expiration policy is not specified, the default of 31 days is used as per: | |||||
| // https://github.com/googleapis/googleapis/blob/51145ff7812d2bb44c1219d0b76dac92a8bd94b2/google/pubsub/v1/pubsub.proto#L605-L606 | |||||
| defaultExpirationPolicy = 31 * 24 * time.Hour | |||||
| ) | |||||
| func (cfg *SubscriptionConfigToUpdate) validate() error { | |||||
| if cfg == nil || cfg.ExpirationPolicy == nil { | |||||
| return nil | |||||
| } | |||||
| policy, min := optional.ToDuration(cfg.ExpirationPolicy), minExpirationPolicy | |||||
| if policy == 0 || policy >= min { | |||||
| return nil | |||||
| } | |||||
| return fmt.Errorf("invalid expiration policy(%q) < minimum(%q)", policy, min) | |||||
| } | |||||
| func expirationPolicyToProto(expirationPolicy optional.Duration) *pb.ExpirationPolicy { | |||||
| if expirationPolicy == nil { | |||||
| return nil | |||||
| } | |||||
| dur := optional.ToDuration(expirationPolicy) | |||||
| var ttl *durpb.Duration | |||||
| // As per: | |||||
| // https://godoc.org/google.golang.org/genproto/googleapis/pubsub/v1#ExpirationPolicy.Ttl | |||||
| // if ExpirationPolicy.Ttl is set to nil, the expirationPolicy is toggled to NEVER expire. | |||||
| if dur != 0 { | |||||
| ttl = ptypes.DurationProto(dur) | |||||
| } | |||||
| return &pb.ExpirationPolicy{ | |||||
| Ttl: ttl, | |||||
| } | |||||
| } | |||||
| // IAM returns the subscription's IAM handle. | |||||
| func (s *Subscription) IAM() *iam.Handle { | |||||
| return iam.InternalNewHandle(s.c.subc.Connection(), s.name) | |||||
| } | |||||
| // CreateSubscription creates a new subscription on a topic. | |||||
| // | |||||
| // id is the name of the subscription to create. It must start with a letter, | |||||
| // and contain only letters ([A-Za-z]), numbers ([0-9]), dashes (-), | |||||
| // underscores (_), periods (.), tildes (~), plus (+) or percent signs (%). It | |||||
| // must be between 3 and 255 characters in length, and must not start with | |||||
| // "goog". | |||||
| // | |||||
| // cfg.Topic is the topic from which the subscription should receive messages. It | |||||
| // need not belong to the same project as the subscription. This field is required. | |||||
| // | |||||
| // cfg.AckDeadline is the maximum time after a subscriber receives a message before | |||||
| // the subscriber should acknowledge the message. It must be between 10 and 600 | |||||
| // seconds (inclusive), and is rounded down to the nearest second. If the | |||||
| // provided ackDeadline is 0, then the default value of 10 seconds is used. | |||||
| // Note: messages which are obtained via Subscription.Receive need not be | |||||
| // acknowledged within this deadline, as the deadline will be automatically | |||||
| // extended. | |||||
| // | |||||
| // cfg.PushConfig may be set to configure this subscription for push delivery. | |||||
| // | |||||
| // If the subscription already exists an error will be returned. | |||||
| func (c *Client) CreateSubscription(ctx context.Context, id string, cfg SubscriptionConfig) (*Subscription, error) { | |||||
| if cfg.Topic == nil { | |||||
| return nil, errors.New("pubsub: require non-nil Topic") | |||||
| } | |||||
| if cfg.AckDeadline == 0 { | |||||
| cfg.AckDeadline = 10 * time.Second | |||||
| } | |||||
| if d := cfg.AckDeadline; d < 10*time.Second || d > 600*time.Second { | |||||
| return nil, fmt.Errorf("ack deadline must be between 10 and 600 seconds; got: %v", d) | |||||
| } | |||||
| sub := c.Subscription(id) | |||||
| _, err := c.subc.CreateSubscription(ctx, cfg.toProto(sub.name)) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return sub, nil | |||||
| } | |||||
| var errReceiveInProgress = errors.New("pubsub: Receive already in progress for this subscription") | |||||
| // Receive calls f with the outstanding messages from the subscription. | |||||
| // It blocks until ctx is done, or the service returns a non-retryable error. | |||||
| // | |||||
| // The standard way to terminate a Receive is to cancel its context: | |||||
| // | |||||
| // cctx, cancel := context.WithCancel(ctx) | |||||
| // err := sub.Receive(cctx, callback) | |||||
| // // Call cancel from callback, or another goroutine. | |||||
| // | |||||
| // If the service returns a non-retryable error, Receive returns that error after | |||||
| // all of the outstanding calls to f have returned. If ctx is done, Receive | |||||
| // returns nil after all of the outstanding calls to f have returned and | |||||
| // all messages have been acknowledged or have expired. | |||||
| // | |||||
| // Receive calls f concurrently from multiple goroutines. It is encouraged to | |||||
| // process messages synchronously in f, even if that processing is relatively | |||||
| // time-consuming; Receive will spawn new goroutines for incoming messages, | |||||
| // limited by MaxOutstandingMessages and MaxOutstandingBytes in ReceiveSettings. | |||||
| // | |||||
| // The context passed to f will be canceled when ctx is Done or there is a | |||||
| // fatal service error. | |||||
| // | |||||
| // Receive will send an ack deadline extension on message receipt, then | |||||
| // automatically extend the ack deadline of all fetched Messages up to the | |||||
| // period specified by s.ReceiveSettings.MaxExtension. | |||||
| // | |||||
| // Each Subscription may have only one invocation of Receive active at a time. | |||||
| func (s *Subscription) Receive(ctx context.Context, f func(context.Context, *Message)) error { | |||||
| s.mu.Lock() | |||||
| if s.receiveActive { | |||||
| s.mu.Unlock() | |||||
| return errReceiveInProgress | |||||
| } | |||||
| s.receiveActive = true | |||||
| s.mu.Unlock() | |||||
| defer func() { s.mu.Lock(); s.receiveActive = false; s.mu.Unlock() }() | |||||
| maxCount := s.ReceiveSettings.MaxOutstandingMessages | |||||
| if maxCount == 0 { | |||||
| maxCount = DefaultReceiveSettings.MaxOutstandingMessages | |||||
| } | |||||
| maxBytes := s.ReceiveSettings.MaxOutstandingBytes | |||||
| if maxBytes == 0 { | |||||
| maxBytes = DefaultReceiveSettings.MaxOutstandingBytes | |||||
| } | |||||
| maxExt := s.ReceiveSettings.MaxExtension | |||||
| if maxExt == 0 { | |||||
| maxExt = DefaultReceiveSettings.MaxExtension | |||||
| } else if maxExt < 0 { | |||||
| // If MaxExtension is negative, disable automatic extension. | |||||
| maxExt = 0 | |||||
| } | |||||
| var numGoroutines int | |||||
| switch { | |||||
| case s.ReceiveSettings.Synchronous: | |||||
| numGoroutines = 1 | |||||
| case s.ReceiveSettings.NumGoroutines >= 1: | |||||
| numGoroutines = s.ReceiveSettings.NumGoroutines | |||||
| default: | |||||
| numGoroutines = DefaultReceiveSettings.NumGoroutines | |||||
| } | |||||
| // TODO(jba): add tests that verify that ReceiveSettings are correctly processed. | |||||
| po := &pullOptions{ | |||||
| maxExtension: maxExt, | |||||
| maxPrefetch: trunc32(int64(maxCount)), | |||||
| synchronous: s.ReceiveSettings.Synchronous, | |||||
| } | |||||
| fc := newFlowController(maxCount, maxBytes) | |||||
| // Wait for all goroutines started by Receive to return, so instead of an | |||||
| // obscure goroutine leak we have an obvious blocked call to Receive. | |||||
| group, gctx := errgroup.WithContext(ctx) | |||||
| for i := 0; i < numGoroutines; i++ { | |||||
| group.Go(func() error { | |||||
| return s.receive(gctx, po, fc, f) | |||||
| }) | |||||
| } | |||||
| return group.Wait() | |||||
| } | |||||
| func (s *Subscription) receive(ctx context.Context, po *pullOptions, fc *flowController, f func(context.Context, *Message)) error { | |||||
| // Cancel a sub-context when we return, to kick the context-aware callbacks | |||||
| // and the goroutine below. | |||||
| ctx2, cancel := context.WithCancel(ctx) | |||||
| // The iterator does not use the context passed to Receive. If it did, canceling | |||||
| // that context would immediately stop the iterator without waiting for unacked | |||||
| // messages. | |||||
| iter := newMessageIterator(s.c.subc, s.name, po) | |||||
| // We cannot use errgroup from Receive here. Receive might already be calling group.Wait, | |||||
| // and group.Wait cannot be called concurrently with group.Go. We give each receive() its | |||||
| // own WaitGroup instead. | |||||
| // Since wg.Add is only called from the main goroutine, wg.Wait is guaranteed | |||||
| // to be called after all Adds. | |||||
| var wg sync.WaitGroup | |||||
| wg.Add(1) | |||||
| go func() { | |||||
| <-ctx2.Done() | |||||
| // Call stop when Receive's context is done. | |||||
| // Stop will block until all outstanding messages have been acknowledged | |||||
| // or there was a fatal service error. | |||||
| iter.stop() | |||||
| wg.Done() | |||||
| }() | |||||
| defer wg.Wait() | |||||
| defer cancel() | |||||
| for { | |||||
| var maxToPull int32 // maximum number of messages to pull | |||||
| if po.synchronous { | |||||
| if po.maxPrefetch < 0 { | |||||
| // If there is no limit on the number of messages to pull, use a reasonable default. | |||||
| maxToPull = 1000 | |||||
| } else { | |||||
| // Limit the number of messages in memory to MaxOutstandingMessages | |||||
| // (here, po.maxPrefetch). For each message currently in memory, we have | |||||
| // called fc.acquire but not fc.release: this is fc.count(). The next | |||||
| // call to Pull should fetch no more than the difference between these | |||||
| // values. | |||||
| maxToPull = po.maxPrefetch - int32(fc.count()) | |||||
| if maxToPull <= 0 { | |||||
| // Wait for some callbacks to finish. | |||||
| if err := gax.Sleep(ctx, synchronousWaitTime); err != nil { | |||||
| // Return nil if the context is done, not err. | |||||
| return nil | |||||
| } | |||||
| continue | |||||
| } | |||||
| } | |||||
| } | |||||
| msgs, err := iter.receive(maxToPull) | |||||
| if err == io.EOF { | |||||
| return nil | |||||
| } | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| for i, msg := range msgs { | |||||
| msg := msg | |||||
| // TODO(jba): call acquire closer to when the message is allocated. | |||||
| if err := fc.acquire(ctx, len(msg.Data)); err != nil { | |||||
| // TODO(jba): test that these "orphaned" messages are nacked immediately when ctx is done. | |||||
| for _, m := range msgs[i:] { | |||||
| m.Nack() | |||||
| } | |||||
| // Return nil if the context is done, not err. | |||||
| return nil | |||||
| } | |||||
| old := msg.doneFunc | |||||
| msgLen := len(msg.Data) | |||||
| msg.doneFunc = func(ackID string, ack bool, receiveTime time.Time) { | |||||
| defer fc.release(msgLen) | |||||
| old(ackID, ack, receiveTime) | |||||
| } | |||||
| wg.Add(1) | |||||
| go func() { | |||||
| defer wg.Done() | |||||
| f(ctx2, msg) | |||||
| }() | |||||
| } | |||||
| } | |||||
| } | |||||
| type pullOptions struct { | |||||
| maxExtension time.Duration | |||||
| maxPrefetch int32 | |||||
| // If true, use unary Pull instead of StreamingPull, and never pull more | |||||
| // than maxPrefetch messages. | |||||
| synchronous bool | |||||
| } | |||||
| @@ -0,0 +1,550 @@ | |||||
| // Copyright 2016 Google LLC | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // http://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| package pubsub | |||||
| import ( | |||||
| "context" | |||||
| "errors" | |||||
| "fmt" | |||||
| "log" | |||||
| "runtime" | |||||
| "strings" | |||||
| "sync" | |||||
| "time" | |||||
| "cloud.google.com/go/iam" | |||||
| "github.com/golang/protobuf/proto" | |||||
| gax "github.com/googleapis/gax-go/v2" | |||||
| "go.opencensus.io/stats" | |||||
| "go.opencensus.io/tag" | |||||
| "google.golang.org/api/support/bundler" | |||||
| pb "google.golang.org/genproto/googleapis/pubsub/v1" | |||||
| fmpb "google.golang.org/genproto/protobuf/field_mask" | |||||
| "google.golang.org/grpc" | |||||
| "google.golang.org/grpc/codes" | |||||
| "google.golang.org/grpc/status" | |||||
| ) | |||||
| const ( | |||||
| // MaxPublishRequestCount is the maximum number of messages that can be in | |||||
| // a single publish request, as defined by the PubSub service. | |||||
| MaxPublishRequestCount = 1000 | |||||
| // MaxPublishRequestBytes is the maximum size of a single publish request | |||||
| // in bytes, as defined by the PubSub service. | |||||
| MaxPublishRequestBytes = 1e7 | |||||
| ) | |||||
| // ErrOversizedMessage indicates that a message's size exceeds MaxPublishRequestBytes. | |||||
| var ErrOversizedMessage = bundler.ErrOversizedItem | |||||
| // Topic is a reference to a PubSub topic. | |||||
| // | |||||
| // The methods of Topic are safe for use by multiple goroutines. | |||||
| type Topic struct { | |||||
| c *Client | |||||
| // The fully qualified identifier for the topic, in the format "projects/<projid>/topics/<name>" | |||||
| name string | |||||
| // Settings for publishing messages. All changes must be made before the | |||||
| // first call to Publish. The default is DefaultPublishSettings. | |||||
| PublishSettings PublishSettings | |||||
| mu sync.RWMutex | |||||
| stopped bool | |||||
| bundler *bundler.Bundler | |||||
| } | |||||
| // PublishSettings control the bundling of published messages. | |||||
| type PublishSettings struct { | |||||
| // Publish a non-empty batch after this delay has passed. | |||||
| DelayThreshold time.Duration | |||||
| // Publish a batch when it has this many messages. The maximum is | |||||
| // MaxPublishRequestCount. | |||||
| CountThreshold int | |||||
| // Publish a batch when its size in bytes reaches this value. | |||||
| ByteThreshold int | |||||
| // The number of goroutines that invoke the Publish RPC concurrently. | |||||
| // | |||||
| // Defaults to a multiple of GOMAXPROCS. | |||||
| NumGoroutines int | |||||
| // The maximum time that the client will attempt to publish a bundle of messages. | |||||
| Timeout time.Duration | |||||
| // The maximum number of bytes that the Bundler will keep in memory before | |||||
| // returning ErrOverflow. | |||||
| // | |||||
| // Defaults to DefaultPublishSettings.BufferedByteLimit. | |||||
| BufferedByteLimit int | |||||
| } | |||||
| // DefaultPublishSettings holds the default values for topics' PublishSettings. | |||||
| var DefaultPublishSettings = PublishSettings{ | |||||
| DelayThreshold: 1 * time.Millisecond, | |||||
| CountThreshold: 100, | |||||
| ByteThreshold: 1e6, | |||||
| Timeout: 60 * time.Second, | |||||
| // By default, limit the bundler to 10 times the max message size. The number 10 is | |||||
| // chosen as a reasonable amount of messages in the worst case whilst still | |||||
| // capping the number to a low enough value to not OOM users. | |||||
| BufferedByteLimit: 10 * MaxPublishRequestBytes, | |||||
| } | |||||
| // CreateTopic creates a new topic. | |||||
| // | |||||
| // The specified topic ID must start with a letter, and contain only letters | |||||
| // ([A-Za-z]), numbers ([0-9]), dashes (-), underscores (_), periods (.), | |||||
| // tildes (~), plus (+) or percent signs (%). It must be between 3 and 255 | |||||
| // characters in length, and must not start with "goog". For more information, | |||||
| // see: https://cloud.google.com/pubsub/docs/admin#resource_names | |||||
| // | |||||
| // If the topic already exists an error will be returned. | |||||
| func (c *Client) CreateTopic(ctx context.Context, topicID string) (*Topic, error) { | |||||
| t := c.Topic(topicID) | |||||
| _, err := c.pubc.CreateTopic(ctx, &pb.Topic{Name: t.name}) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return t, nil | |||||
| } | |||||
| // CreateTopicWithConfig creates a topic from TopicConfig. | |||||
| // | |||||
| // The specified topic ID must start with a letter, and contain only letters | |||||
| // ([A-Za-z]), numbers ([0-9]), dashes (-), underscores (_), periods (.), | |||||
| // tildes (~), plus (+) or percent signs (%). It must be between 3 and 255 | |||||
| // characters in length, and must not start with "goog". For more information, | |||||
| // see: https://cloud.google.com/pubsub/docs/admin#resource_names. | |||||
| // | |||||
| // If the topic already exists, an error will be returned. | |||||
| func (c *Client) CreateTopicWithConfig(ctx context.Context, topicID string, tc *TopicConfig) (*Topic, error) { | |||||
| t := c.Topic(topicID) | |||||
| _, err := c.pubc.CreateTopic(ctx, &pb.Topic{ | |||||
| Name: t.name, | |||||
| Labels: tc.Labels, | |||||
| MessageStoragePolicy: messageStoragePolicyToProto(&tc.MessageStoragePolicy), | |||||
| KmsKeyName: tc.KMSKeyName, | |||||
| }) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return t, nil | |||||
| } | |||||
| // Topic creates a reference to a topic in the client's project. | |||||
| // | |||||
| // If a Topic's Publish method is called, it has background goroutines | |||||
| // associated with it. Clean them up by calling Topic.Stop. | |||||
| // | |||||
| // Avoid creating many Topic instances if you use them to publish. | |||||
| func (c *Client) Topic(id string) *Topic { | |||||
| return c.TopicInProject(id, c.projectID) | |||||
| } | |||||
| // TopicInProject creates a reference to a topic in the given project. | |||||
| // | |||||
| // If a Topic's Publish method is called, it has background goroutines | |||||
| // associated with it. Clean them up by calling Topic.Stop. | |||||
| // | |||||
| // Avoid creating many Topic instances if you use them to publish. | |||||
| func (c *Client) TopicInProject(id, projectID string) *Topic { | |||||
| return newTopic(c, fmt.Sprintf("projects/%s/topics/%s", projectID, id)) | |||||
| } | |||||
| func newTopic(c *Client, name string) *Topic { | |||||
| return &Topic{ | |||||
| c: c, | |||||
| name: name, | |||||
| PublishSettings: DefaultPublishSettings, | |||||
| } | |||||
| } | |||||
| // TopicConfig describes the configuration of a topic. | |||||
| type TopicConfig struct { | |||||
| // The set of labels for the topic. | |||||
| Labels map[string]string | |||||
| // The topic's message storage policy. | |||||
| MessageStoragePolicy MessageStoragePolicy | |||||
| // The name of the Cloud KMS key to be used to protect access to messages | |||||
| // published to this topic, in the format | |||||
| // "projects/P/locations/L/keyRings/R/cryptoKeys/K". | |||||
| KMSKeyName string | |||||
| } | |||||
| // TopicConfigToUpdate describes how to update a topic. | |||||
| type TopicConfigToUpdate struct { | |||||
| // If non-nil, the current set of labels is completely | |||||
| // replaced by the new set. | |||||
| Labels map[string]string | |||||
| // If non-nil, the existing policy (containing the list of regions) | |||||
| // is completely replaced by the new policy. | |||||
| // | |||||
| // Use the zero value &MessageStoragePolicy{} to reset the topic back to | |||||
| // using the organization's Resource Location Restriction policy. | |||||
| // | |||||
| // If nil, the policy remains unchanged. | |||||
| // | |||||
| // This field has beta status. It is not subject to the stability guarantee | |||||
| // and may change. | |||||
| MessageStoragePolicy *MessageStoragePolicy | |||||
| } | |||||
| func protoToTopicConfig(pbt *pb.Topic) TopicConfig { | |||||
| return TopicConfig{ | |||||
| Labels: pbt.Labels, | |||||
| MessageStoragePolicy: protoToMessageStoragePolicy(pbt.MessageStoragePolicy), | |||||
| KMSKeyName: pbt.KmsKeyName, | |||||
| } | |||||
| } | |||||
| // MessageStoragePolicy constrains how messages published to the topic may be stored. It | |||||
| // is determined when the topic is created based on the policy configured at | |||||
| // the project level. | |||||
| type MessageStoragePolicy struct { | |||||
| // AllowedPersistenceRegions is the list of GCP regions where messages that are published | |||||
| // to the topic may be persisted in storage. Messages published by publishers running in | |||||
| // non-allowed GCP regions (or running outside of GCP altogether) will be | |||||
| // routed for storage in one of the allowed regions. | |||||
| // | |||||
| // If empty, it indicates a misconfiguration at the project or organization level, which | |||||
| // will result in all Publish operations failing. This field cannot be empty in updates. | |||||
| // | |||||
| // If nil, then the policy is not defined on a topic level. When used in updates, it resets | |||||
| // the regions back to the organization level Resource Location Restriction policy. | |||||
| // | |||||
| // For more information, see | |||||
| // https://cloud.google.com/pubsub/docs/resource-location-restriction#pubsub-storage-locations. | |||||
| AllowedPersistenceRegions []string | |||||
| } | |||||
| func protoToMessageStoragePolicy(msp *pb.MessageStoragePolicy) MessageStoragePolicy { | |||||
| if msp == nil { | |||||
| return MessageStoragePolicy{} | |||||
| } | |||||
| return MessageStoragePolicy{AllowedPersistenceRegions: msp.AllowedPersistenceRegions} | |||||
| } | |||||
| func messageStoragePolicyToProto(msp *MessageStoragePolicy) *pb.MessageStoragePolicy { | |||||
| if msp == nil || msp.AllowedPersistenceRegions == nil { | |||||
| return nil | |||||
| } | |||||
| return &pb.MessageStoragePolicy{AllowedPersistenceRegions: msp.AllowedPersistenceRegions} | |||||
| } | |||||
| // Config returns the TopicConfig for the topic. | |||||
| func (t *Topic) Config(ctx context.Context) (TopicConfig, error) { | |||||
| pbt, err := t.c.pubc.GetTopic(ctx, &pb.GetTopicRequest{Topic: t.name}) | |||||
| if err != nil { | |||||
| return TopicConfig{}, err | |||||
| } | |||||
| return protoToTopicConfig(pbt), nil | |||||
| } | |||||
| // Update changes an existing topic according to the fields set in cfg. It returns | |||||
| // the new TopicConfig. | |||||
| func (t *Topic) Update(ctx context.Context, cfg TopicConfigToUpdate) (TopicConfig, error) { | |||||
| req := t.updateRequest(cfg) | |||||
| if len(req.UpdateMask.Paths) == 0 { | |||||
| return TopicConfig{}, errors.New("pubsub: UpdateTopic call with nothing to update") | |||||
| } | |||||
| rpt, err := t.c.pubc.UpdateTopic(ctx, req) | |||||
| if err != nil { | |||||
| return TopicConfig{}, err | |||||
| } | |||||
| return protoToTopicConfig(rpt), nil | |||||
| } | |||||
| func (t *Topic) updateRequest(cfg TopicConfigToUpdate) *pb.UpdateTopicRequest { | |||||
| pt := &pb.Topic{Name: t.name} | |||||
| var paths []string | |||||
| if cfg.Labels != nil { | |||||
| pt.Labels = cfg.Labels | |||||
| paths = append(paths, "labels") | |||||
| } | |||||
| if cfg.MessageStoragePolicy != nil { | |||||
| pt.MessageStoragePolicy = messageStoragePolicyToProto(cfg.MessageStoragePolicy) | |||||
| paths = append(paths, "message_storage_policy") | |||||
| } | |||||
| return &pb.UpdateTopicRequest{ | |||||
| Topic: pt, | |||||
| UpdateMask: &fmpb.FieldMask{Paths: paths}, | |||||
| } | |||||
| } | |||||
| // Topics returns an iterator which returns all of the topics for the client's project. | |||||
| func (c *Client) Topics(ctx context.Context) *TopicIterator { | |||||
| it := c.pubc.ListTopics(ctx, &pb.ListTopicsRequest{Project: c.fullyQualifiedProjectName()}) | |||||
| return &TopicIterator{ | |||||
| c: c, | |||||
| next: func() (string, error) { | |||||
| topic, err := it.Next() | |||||
| if err != nil { | |||||
| return "", err | |||||
| } | |||||
| return topic.Name, nil | |||||
| }, | |||||
| } | |||||
| } | |||||
| // TopicIterator is an iterator that returns a series of topics. | |||||
| type TopicIterator struct { | |||||
| c *Client | |||||
| next func() (string, error) | |||||
| } | |||||
| // Next returns the next topic. If there are no more topics, iterator.Done will be returned. | |||||
| func (tps *TopicIterator) Next() (*Topic, error) { | |||||
| topicName, err := tps.next() | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return newTopic(tps.c, topicName), nil | |||||
| } | |||||
| // ID returns the unique identifier of the topic within its project. | |||||
| func (t *Topic) ID() string { | |||||
| slash := strings.LastIndex(t.name, "/") | |||||
| if slash == -1 { | |||||
| // name is not a fully-qualified name. | |||||
| panic("bad topic name") | |||||
| } | |||||
| return t.name[slash+1:] | |||||
| } | |||||
| // String returns the printable globally unique name for the topic. | |||||
| func (t *Topic) String() string { | |||||
| return t.name | |||||
| } | |||||
| // Delete deletes the topic. | |||||
| func (t *Topic) Delete(ctx context.Context) error { | |||||
| return t.c.pubc.DeleteTopic(ctx, &pb.DeleteTopicRequest{Topic: t.name}) | |||||
| } | |||||
| // Exists reports whether the topic exists on the server. | |||||
| func (t *Topic) Exists(ctx context.Context) (bool, error) { | |||||
| if t.name == "_deleted-topic_" { | |||||
| return false, nil | |||||
| } | |||||
| _, err := t.c.pubc.GetTopic(ctx, &pb.GetTopicRequest{Topic: t.name}) | |||||
| if err == nil { | |||||
| return true, nil | |||||
| } | |||||
| if status.Code(err) == codes.NotFound { | |||||
| return false, nil | |||||
| } | |||||
| return false, err | |||||
| } | |||||
| // IAM returns the topic's IAM handle. | |||||
| func (t *Topic) IAM() *iam.Handle { | |||||
| return iam.InternalNewHandle(t.c.pubc.Connection(), t.name) | |||||
| } | |||||
| // Subscriptions returns an iterator which returns the subscriptions for this topic. | |||||
| // | |||||
| // Some of the returned subscriptions may belong to a project other than t. | |||||
| func (t *Topic) Subscriptions(ctx context.Context) *SubscriptionIterator { | |||||
| it := t.c.pubc.ListTopicSubscriptions(ctx, &pb.ListTopicSubscriptionsRequest{ | |||||
| Topic: t.name, | |||||
| }) | |||||
| return &SubscriptionIterator{ | |||||
| c: t.c, | |||||
| next: it.Next, | |||||
| } | |||||
| } | |||||
| var errTopicStopped = errors.New("pubsub: Stop has been called for this topic") | |||||
| // Publish publishes msg to the topic asynchronously. Messages are batched and | |||||
| // sent according to the topic's PublishSettings. Publish never blocks. | |||||
| // | |||||
| // Publish returns a non-nil PublishResult which will be ready when the | |||||
| // message has been sent (or has failed to be sent) to the server. | |||||
| // | |||||
| // Publish creates goroutines for batching and sending messages. These goroutines | |||||
| // need to be stopped by calling t.Stop(). Once stopped, future calls to Publish | |||||
| // will immediately return a PublishResult with an error. | |||||
| func (t *Topic) Publish(ctx context.Context, msg *Message) *PublishResult { | |||||
| // TODO(jba): if this turns out to take significant time, try to approximate it. | |||||
| // Or, convert the messages to protos in Publish, instead of in the service. | |||||
| msg.size = proto.Size(&pb.PubsubMessage{ | |||||
| Data: msg.Data, | |||||
| Attributes: msg.Attributes, | |||||
| }) | |||||
| r := &PublishResult{ready: make(chan struct{})} | |||||
| t.initBundler() | |||||
| t.mu.RLock() | |||||
| defer t.mu.RUnlock() | |||||
| // TODO(aboulhosn) [from bcmills] consider changing the semantics of bundler to perform this logic so we don't have to do it here | |||||
| if t.stopped { | |||||
| r.set("", errTopicStopped) | |||||
| return r | |||||
| } | |||||
| // TODO(jba) [from bcmills] consider using a shared channel per bundle | |||||
| // (requires Bundler API changes; would reduce allocations) | |||||
| err := t.bundler.Add(&bundledMessage{msg, r}, msg.size) | |||||
| if err != nil { | |||||
| r.set("", err) | |||||
| } | |||||
| return r | |||||
| } | |||||
| // Stop sends all remaining published messages and stop goroutines created for handling | |||||
| // publishing. Returns once all outstanding messages have been sent or have | |||||
| // failed to be sent. | |||||
| func (t *Topic) Stop() { | |||||
| t.mu.Lock() | |||||
| noop := t.stopped || t.bundler == nil | |||||
| t.stopped = true | |||||
| t.mu.Unlock() | |||||
| if noop { | |||||
| return | |||||
| } | |||||
| t.bundler.Flush() | |||||
| } | |||||
| // A PublishResult holds the result from a call to Publish. | |||||
| type PublishResult struct { | |||||
| ready chan struct{} | |||||
| serverID string | |||||
| err error | |||||
| } | |||||
| // Ready returns a channel that is closed when the result is ready. | |||||
| // When the Ready channel is closed, Get is guaranteed not to block. | |||||
| func (r *PublishResult) Ready() <-chan struct{} { return r.ready } | |||||
| // Get returns the server-generated message ID and/or error result of a Publish call. | |||||
| // Get blocks until the Publish call completes or the context is done. | |||||
| func (r *PublishResult) Get(ctx context.Context) (serverID string, err error) { | |||||
| // If the result is already ready, return it even if the context is done. | |||||
| select { | |||||
| case <-r.Ready(): | |||||
| return r.serverID, r.err | |||||
| default: | |||||
| } | |||||
| select { | |||||
| case <-ctx.Done(): | |||||
| return "", ctx.Err() | |||||
| case <-r.Ready(): | |||||
| return r.serverID, r.err | |||||
| } | |||||
| } | |||||
| func (r *PublishResult) set(sid string, err error) { | |||||
| r.serverID = sid | |||||
| r.err = err | |||||
| close(r.ready) | |||||
| } | |||||
| type bundledMessage struct { | |||||
| msg *Message | |||||
| res *PublishResult | |||||
| } | |||||
| func (t *Topic) initBundler() { | |||||
| t.mu.RLock() | |||||
| noop := t.stopped || t.bundler != nil | |||||
| t.mu.RUnlock() | |||||
| if noop { | |||||
| return | |||||
| } | |||||
| t.mu.Lock() | |||||
| defer t.mu.Unlock() | |||||
| // Must re-check, since we released the lock. | |||||
| if t.stopped || t.bundler != nil { | |||||
| return | |||||
| } | |||||
| timeout := t.PublishSettings.Timeout | |||||
| t.bundler = bundler.NewBundler(&bundledMessage{}, func(items interface{}) { | |||||
| // TODO(jba): use a context detached from the one passed to NewClient. | |||||
| ctx := context.TODO() | |||||
| if timeout != 0 { | |||||
| var cancel func() | |||||
| ctx, cancel = context.WithTimeout(ctx, timeout) | |||||
| defer cancel() | |||||
| } | |||||
| t.publishMessageBundle(ctx, items.([]*bundledMessage)) | |||||
| }) | |||||
| t.bundler.DelayThreshold = t.PublishSettings.DelayThreshold | |||||
| t.bundler.BundleCountThreshold = t.PublishSettings.CountThreshold | |||||
| if t.bundler.BundleCountThreshold > MaxPublishRequestCount { | |||||
| t.bundler.BundleCountThreshold = MaxPublishRequestCount | |||||
| } | |||||
| t.bundler.BundleByteThreshold = t.PublishSettings.ByteThreshold | |||||
| bufferedByteLimit := DefaultPublishSettings.BufferedByteLimit | |||||
| if t.PublishSettings.BufferedByteLimit > 0 { | |||||
| bufferedByteLimit = t.PublishSettings.BufferedByteLimit | |||||
| } | |||||
| t.bundler.BufferedByteLimit = bufferedByteLimit | |||||
| t.bundler.BundleByteLimit = MaxPublishRequestBytes | |||||
| // Unless overridden, allow many goroutines per CPU to call the Publish RPC concurrently. | |||||
| // The default value was determined via extensive load testing (see the loadtest subdirectory). | |||||
| if t.PublishSettings.NumGoroutines > 0 { | |||||
| t.bundler.HandlerLimit = t.PublishSettings.NumGoroutines | |||||
| } else { | |||||
| t.bundler.HandlerLimit = 25 * runtime.GOMAXPROCS(0) | |||||
| } | |||||
| } | |||||
| func (t *Topic) publishMessageBundle(ctx context.Context, bms []*bundledMessage) { | |||||
| ctx, err := tag.New(ctx, tag.Insert(keyStatus, "OK"), tag.Upsert(keyTopic, t.name)) | |||||
| if err != nil { | |||||
| log.Printf("pubsub: cannot create context with tag in publishMessageBundle: %v", err) | |||||
| } | |||||
| pbMsgs := make([]*pb.PubsubMessage, len(bms)) | |||||
| for i, bm := range bms { | |||||
| pbMsgs[i] = &pb.PubsubMessage{ | |||||
| Data: bm.msg.Data, | |||||
| Attributes: bm.msg.Attributes, | |||||
| } | |||||
| bm.msg = nil // release bm.msg for GC | |||||
| } | |||||
| start := time.Now() | |||||
| res, err := t.c.pubc.Publish(ctx, &pb.PublishRequest{ | |||||
| Topic: t.name, | |||||
| Messages: pbMsgs, | |||||
| }, gax.WithGRPCOptions(grpc.MaxCallSendMsgSize(maxSendRecvBytes))) | |||||
| end := time.Now() | |||||
| if err != nil { | |||||
| // Update context with error tag for OpenCensus, | |||||
| // using same stats.Record() call as success case. | |||||
| ctx, _ = tag.New(ctx, tag.Upsert(keyStatus, "ERROR"), | |||||
| tag.Upsert(keyError, err.Error())) | |||||
| } | |||||
| stats.Record(ctx, | |||||
| PublishLatency.M(float64(end.Sub(start)/time.Millisecond)), | |||||
| PublishedMessages.M(int64(len(bms)))) | |||||
| for i, bm := range bms { | |||||
| if err != nil { | |||||
| bm.res.set("", err) | |||||
| } else { | |||||
| bm.res.set(res.MessageIds[i], nil) | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,217 @@ | |||||
| // Copyright 2018 Google LLC | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // http://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| package pubsub | |||||
| import ( | |||||
| "context" | |||||
| "log" | |||||
| "sync" | |||||
| "go.opencensus.io/plugin/ocgrpc" | |||||
| "go.opencensus.io/stats" | |||||
| "go.opencensus.io/stats/view" | |||||
| "go.opencensus.io/tag" | |||||
| "google.golang.org/api/option" | |||||
| "google.golang.org/grpc" | |||||
| ) | |||||
| func openCensusOptions() []option.ClientOption { | |||||
| return []option.ClientOption{ | |||||
| option.WithGRPCDialOption(grpc.WithStatsHandler(&ocgrpc.ClientHandler{})), | |||||
| } | |||||
| } | |||||
| // The following keys are used to tag requests with a specific topic/subscription ID. | |||||
| var ( | |||||
| keyTopic = tag.MustNewKey("topic") | |||||
| keySubscription = tag.MustNewKey("subscription") | |||||
| ) | |||||
| // In the following, errors are used if status is not "OK". | |||||
| var ( | |||||
| keyStatus = tag.MustNewKey("status") | |||||
| keyError = tag.MustNewKey("error") | |||||
| ) | |||||
| const statsPrefix = "cloud.google.com/go/pubsub/" | |||||
| // The following are measures recorded in publish/subscribe flows. | |||||
| var ( | |||||
| // PublishedMessages is a measure of the number of messages published, which may include errors. | |||||
| // It is EXPERIMENTAL and subject to change or removal without notice. | |||||
| PublishedMessages = stats.Int64(statsPrefix+"published_messages", "Number of PubSub message published", stats.UnitDimensionless) | |||||
| // PublishLatency is a measure of the number of milliseconds it took to publish a bundle, | |||||
| // which may consist of one or more messages. | |||||
| // It is EXPERIMENTAL and subject to change or removal without notice. | |||||
| PublishLatency = stats.Float64(statsPrefix+"publish_roundtrip_latency", "The latency in milliseconds per publish batch", stats.UnitMilliseconds) | |||||
| // PullCount is a measure of the number of messages pulled. | |||||
| // It is EXPERIMENTAL and subject to change or removal without notice. | |||||
| PullCount = stats.Int64(statsPrefix+"pull_count", "Number of PubSub messages pulled", stats.UnitDimensionless) | |||||
| // AckCount is a measure of the number of messages acked. | |||||
| // It is EXPERIMENTAL and subject to change or removal without notice. | |||||
| AckCount = stats.Int64(statsPrefix+"ack_count", "Number of PubSub messages acked", stats.UnitDimensionless) | |||||
| // NackCount is a measure of the number of messages nacked. | |||||
| // It is EXPERIMENTAL and subject to change or removal without notice. | |||||
| NackCount = stats.Int64(statsPrefix+"nack_count", "Number of PubSub messages nacked", stats.UnitDimensionless) | |||||
| // ModAckCount is a measure of the number of messages whose ack-deadline was modified. | |||||
| // It is EXPERIMENTAL and subject to change or removal without notice. | |||||
| ModAckCount = stats.Int64(statsPrefix+"mod_ack_count", "Number of ack-deadlines modified", stats.UnitDimensionless) | |||||
| // ModAckTimeoutCount is a measure of the number ModifyAckDeadline RPCs that timed out. | |||||
| // It is EXPERIMENTAL and subject to change or removal without notice. | |||||
| ModAckTimeoutCount = stats.Int64(statsPrefix+"mod_ack_timeout_count", "Number of ModifyAckDeadline RPCs that timed out", stats.UnitDimensionless) | |||||
| // StreamOpenCount is a measure of the number of times a streaming-pull stream was opened. | |||||
| // It is EXPERIMENTAL and subject to change or removal without notice. | |||||
| StreamOpenCount = stats.Int64(statsPrefix+"stream_open_count", "Number of calls opening a new streaming pull", stats.UnitDimensionless) | |||||
| // StreamRetryCount is a measure of the number of times a streaming-pull operation was retried. | |||||
| // It is EXPERIMENTAL and subject to change or removal without notice. | |||||
| StreamRetryCount = stats.Int64(statsPrefix+"stream_retry_count", "Number of retries of a stream send or receive", stats.UnitDimensionless) | |||||
| // StreamRequestCount is a measure of the number of requests sent on a streaming-pull stream. | |||||
| // It is EXPERIMENTAL and subject to change or removal without notice. | |||||
| StreamRequestCount = stats.Int64(statsPrefix+"stream_request_count", "Number gRPC StreamingPull request messages sent", stats.UnitDimensionless) | |||||
| // StreamResponseCount is a measure of the number of responses received on a streaming-pull stream. | |||||
| // It is EXPERIMENTAL and subject to change or removal without notice. | |||||
| StreamResponseCount = stats.Int64(statsPrefix+"stream_response_count", "Number of gRPC StreamingPull response messages received", stats.UnitDimensionless) | |||||
| ) | |||||
| var ( | |||||
| // PublishedMessagesView is a cumulative sum of PublishedMessages. | |||||
| // It is EXPERIMENTAL and subject to change or removal without notice. | |||||
| PublishedMessagesView *view.View | |||||
| // PublishLatencyView is a distribution of PublishLatency. | |||||
| // It is EXPERIMENTAL and subject to change or removal without notice. | |||||
| PublishLatencyView *view.View | |||||
| // PullCountView is a cumulative sum of PullCount. | |||||
| // It is EXPERIMENTAL and subject to change or removal without notice. | |||||
| PullCountView *view.View | |||||
| // AckCountView is a cumulative sum of AckCount. | |||||
| // It is EXPERIMENTAL and subject to change or removal without notice. | |||||
| AckCountView *view.View | |||||
| // NackCountView is a cumulative sum of NackCount. | |||||
| // It is EXPERIMENTAL and subject to change or removal without notice. | |||||
| NackCountView *view.View | |||||
| // ModAckCountView is a cumulative sum of ModAckCount. | |||||
| // It is EXPERIMENTAL and subject to change or removal without notice. | |||||
| ModAckCountView *view.View | |||||
| // ModAckTimeoutCountView is a cumulative sum of ModAckTimeoutCount. | |||||
| // It is EXPERIMENTAL and subject to change or removal without notice. | |||||
| ModAckTimeoutCountView *view.View | |||||
| // StreamOpenCountView is a cumulative sum of StreamOpenCount. | |||||
| // It is EXPERIMENTAL and subject to change or removal without notice. | |||||
| StreamOpenCountView *view.View | |||||
| // StreamRetryCountView is a cumulative sum of StreamRetryCount. | |||||
| // It is EXPERIMENTAL and subject to change or removal without notice. | |||||
| StreamRetryCountView *view.View | |||||
| // StreamRequestCountView is a cumulative sum of StreamRequestCount. | |||||
| // It is EXPERIMENTAL and subject to change or removal without notice. | |||||
| StreamRequestCountView *view.View | |||||
| // StreamResponseCountView is a cumulative sum of StreamResponseCount. | |||||
| // It is EXPERIMENTAL and subject to change or removal without notice. | |||||
| StreamResponseCountView *view.View | |||||
| ) | |||||
| func init() { | |||||
| PublishedMessagesView = createCountView(stats.Measure(PublishedMessages), keyTopic, keyStatus, keyError) | |||||
| PublishLatencyView = createDistView(PublishLatency, keyTopic, keyStatus, keyError) | |||||
| PullCountView = createCountView(PullCount, keySubscription) | |||||
| AckCountView = createCountView(AckCount, keySubscription) | |||||
| NackCountView = createCountView(NackCount, keySubscription) | |||||
| ModAckCountView = createCountView(ModAckCount, keySubscription) | |||||
| ModAckTimeoutCountView = createCountView(ModAckTimeoutCount, keySubscription) | |||||
| StreamOpenCountView = createCountView(StreamOpenCount, keySubscription) | |||||
| StreamRetryCountView = createCountView(StreamRetryCount, keySubscription) | |||||
| StreamRequestCountView = createCountView(StreamRequestCount, keySubscription) | |||||
| StreamResponseCountView = createCountView(StreamResponseCount, keySubscription) | |||||
| DefaultPublishViews = []*view.View{ | |||||
| PublishedMessagesView, | |||||
| PublishLatencyView, | |||||
| } | |||||
| DefaultSubscribeViews = []*view.View{ | |||||
| PullCountView, | |||||
| AckCountView, | |||||
| NackCountView, | |||||
| ModAckCountView, | |||||
| ModAckTimeoutCountView, | |||||
| StreamOpenCountView, | |||||
| StreamRetryCountView, | |||||
| StreamRequestCountView, | |||||
| StreamResponseCountView, | |||||
| } | |||||
| } | |||||
| // The following arrays are the default views related to publish/subscribe operations provided by this package. | |||||
| // It is EXPERIMENTAL and subject to change or removal without notice. | |||||
| var ( | |||||
| DefaultPublishViews []*view.View | |||||
| DefaultSubscribeViews []*view.View | |||||
| ) | |||||
| func createCountView(m stats.Measure, keys ...tag.Key) *view.View { | |||||
| return &view.View{ | |||||
| Name: m.Name(), | |||||
| Description: m.Description(), | |||||
| TagKeys: keys, | |||||
| Measure: m, | |||||
| Aggregation: view.Sum(), | |||||
| } | |||||
| } | |||||
| func createDistView(m stats.Measure, keys ...tag.Key) *view.View { | |||||
| return &view.View{ | |||||
| Name: m.Name(), | |||||
| Description: m.Description(), | |||||
| TagKeys: keys, | |||||
| Measure: m, | |||||
| Aggregation: view.Distribution(0, 25, 50, 75, 100, 200, 400, 600, 800, 1000, 2000, 4000, 6000), | |||||
| } | |||||
| } | |||||
| var logOnce sync.Once | |||||
| // withSubscriptionKey returns a new context modified with the subscriptionKey tag map. | |||||
| func withSubscriptionKey(ctx context.Context, subName string) context.Context { | |||||
| ctx, err := tag.New(ctx, tag.Upsert(keySubscription, subName)) | |||||
| if err != nil { | |||||
| logOnce.Do(func() { | |||||
| log.Printf("pubsub: error creating tag map for 'subscribe' key: %v", err) | |||||
| }) | |||||
| } | |||||
| return ctx | |||||
| } | |||||
| func recordStat(ctx context.Context, m *stats.Int64Measure, n int64) { | |||||
| stats.Record(ctx, m.M(n)) | |||||
| } | |||||
| @@ -0,0 +1 @@ | |||||
| coverage* | |||||
| @@ -0,0 +1,17 @@ | |||||
| --- | |||||
| language: go | |||||
| go: | |||||
| - 1.11.x | |||||
| env: | |||||
| - GO111MODULE=on | |||||
| services: | |||||
| - docker | |||||
| script: | |||||
| - make test-with-coverage | |||||
| after_success: | |||||
| - bash <(curl -s https://codecov.io/bash) | |||||
| @@ -0,0 +1,354 @@ | |||||
| Mozilla Public License, version 2.0 | |||||
| 1. Definitions | |||||
| 1.1. “Contributor” | |||||
| means each individual or legal entity that creates, contributes to the | |||||
| creation of, or owns Covered Software. | |||||
| 1.2. “Contributor Version” | |||||
| means the combination of the Contributions of others (if any) used by a | |||||
| Contributor and that particular Contributor’s Contribution. | |||||
| 1.3. “Contribution” | |||||
| means Covered Software of a particular Contributor. | |||||
| 1.4. “Covered Software” | |||||
| means Source Code Form to which the initial Contributor has attached the | |||||
| notice in Exhibit A, the Executable Form of such Source Code Form, and | |||||
| Modifications of such Source Code Form, in each case including portions | |||||
| thereof. | |||||
| 1.5. “Incompatible With Secondary Licenses” | |||||
| means | |||||
| a. that the initial Contributor has attached the notice described in | |||||
| Exhibit B to the Covered Software; or | |||||
| b. that the Covered Software was made available under the terms of version | |||||
| 1.1 or earlier of the License, but not also under the terms of a | |||||
| Secondary License. | |||||
| 1.6. “Executable Form” | |||||
| means any form of the work other than Source Code Form. | |||||
| 1.7. “Larger Work” | |||||
| means a work that combines Covered Software with other material, in a separate | |||||
| file or files, that is not Covered Software. | |||||
| 1.8. “License” | |||||
| means this document. | |||||
| 1.9. “Licensable” | |||||
| means having the right to grant, to the maximum extent possible, whether at the | |||||
| time of the initial grant or subsequently, any and all of the rights conveyed by | |||||
| this License. | |||||
| 1.10. “Modifications” | |||||
| means any of the following: | |||||
| a. any file in Source Code Form that results from an addition to, deletion | |||||
| from, or modification of the contents of Covered Software; or | |||||
| b. any new file in Source Code Form that contains any Covered Software. | |||||
| 1.11. “Patent Claims” of a Contributor | |||||
| means any patent claim(s), including without limitation, method, process, | |||||
| and apparatus claims, in any patent Licensable by such Contributor that | |||||
| would be infringed, but for the grant of the License, by the making, | |||||
| using, selling, offering for sale, having made, import, or transfer of | |||||
| either its Contributions or its Contributor Version. | |||||
| 1.12. “Secondary License” | |||||
| means either the GNU General Public License, Version 2.0, the GNU Lesser | |||||
| General Public License, Version 2.1, the GNU Affero General Public | |||||
| License, Version 3.0, or any later versions of those licenses. | |||||
| 1.13. “Source Code Form” | |||||
| means the form of the work preferred for making modifications. | |||||
| 1.14. “You” (or “Your”) | |||||
| means an individual or a legal entity exercising rights under this | |||||
| License. For legal entities, “You” includes any entity that controls, is | |||||
| controlled by, or is under common control with You. For purposes of this | |||||
| definition, “control” means (a) the power, direct or indirect, to cause | |||||
| the direction or management of such entity, whether by contract or | |||||
| otherwise, or (b) ownership of more than fifty percent (50%) of the | |||||
| outstanding shares or beneficial ownership of such entity. | |||||
| 2. License Grants and Conditions | |||||
| 2.1. Grants | |||||
| Each Contributor hereby grants You a world-wide, royalty-free, | |||||
| non-exclusive license: | |||||
| a. under intellectual property rights (other than patent or trademark) | |||||
| Licensable by such Contributor to use, reproduce, make available, | |||||
| modify, display, perform, distribute, and otherwise exploit its | |||||
| Contributions, either on an unmodified basis, with Modifications, or as | |||||
| part of a Larger Work; and | |||||
| b. under Patent Claims of such Contributor to make, use, sell, offer for | |||||
| sale, have made, import, and otherwise transfer either its Contributions | |||||
| or its Contributor Version. | |||||
| 2.2. Effective Date | |||||
| The licenses granted in Section 2.1 with respect to any Contribution become | |||||
| effective for each Contribution on the date the Contributor first distributes | |||||
| such Contribution. | |||||
| 2.3. Limitations on Grant Scope | |||||
| The licenses granted in this Section 2 are the only rights granted under this | |||||
| License. No additional rights or licenses will be implied from the distribution | |||||
| or licensing of Covered Software under this License. Notwithstanding Section | |||||
| 2.1(b) above, no patent license is granted by a Contributor: | |||||
| a. for any code that a Contributor has removed from Covered Software; or | |||||
| b. for infringements caused by: (i) Your and any other third party’s | |||||
| modifications of Covered Software, or (ii) the combination of its | |||||
| Contributions with other software (except as part of its Contributor | |||||
| Version); or | |||||
| c. under Patent Claims infringed by Covered Software in the absence of its | |||||
| Contributions. | |||||
| This License does not grant any rights in the trademarks, service marks, or | |||||
| logos of any Contributor (except as may be necessary to comply with the | |||||
| notice requirements in Section 3.4). | |||||
| 2.4. Subsequent Licenses | |||||
| No Contributor makes additional grants as a result of Your choice to | |||||
| distribute the Covered Software under a subsequent version of this License | |||||
| (see Section 10.2) or under the terms of a Secondary License (if permitted | |||||
| under the terms of Section 3.3). | |||||
| 2.5. Representation | |||||
| Each Contributor represents that the Contributor believes its Contributions | |||||
| are its original creation(s) or it has sufficient rights to grant the | |||||
| rights to its Contributions conveyed by this License. | |||||
| 2.6. Fair Use | |||||
| This License is not intended to limit any rights You have under applicable | |||||
| copyright doctrines of fair use, fair dealing, or other equivalents. | |||||
| 2.7. Conditions | |||||
| Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in | |||||
| Section 2.1. | |||||
| 3. Responsibilities | |||||
| 3.1. Distribution of Source Form | |||||
| All distribution of Covered Software in Source Code Form, including any | |||||
| Modifications that You create or to which You contribute, must be under the | |||||
| terms of this License. You must inform recipients that the Source Code Form | |||||
| of the Covered Software is governed by the terms of this License, and how | |||||
| they can obtain a copy of this License. You may not attempt to alter or | |||||
| restrict the recipients’ rights in the Source Code Form. | |||||
| 3.2. Distribution of Executable Form | |||||
| If You distribute Covered Software in Executable Form then: | |||||
| a. such Covered Software must also be made available in Source Code Form, | |||||
| as described in Section 3.1, and You must inform recipients of the | |||||
| Executable Form how they can obtain a copy of such Source Code Form by | |||||
| reasonable means in a timely manner, at a charge no more than the cost | |||||
| of distribution to the recipient; and | |||||
| b. You may distribute such Executable Form under the terms of this License, | |||||
| or sublicense it under different terms, provided that the license for | |||||
| the Executable Form does not attempt to limit or alter the recipients’ | |||||
| rights in the Source Code Form under this License. | |||||
| 3.3. Distribution of a Larger Work | |||||
| You may create and distribute a Larger Work under terms of Your choice, | |||||
| provided that You also comply with the requirements of this License for the | |||||
| Covered Software. If the Larger Work is a combination of Covered Software | |||||
| with a work governed by one or more Secondary Licenses, and the Covered | |||||
| Software is not Incompatible With Secondary Licenses, this License permits | |||||
| You to additionally distribute such Covered Software under the terms of | |||||
| such Secondary License(s), so that the recipient of the Larger Work may, at | |||||
| their option, further distribute the Covered Software under the terms of | |||||
| either this License or such Secondary License(s). | |||||
| 3.4. Notices | |||||
| You may not remove or alter the substance of any license notices (including | |||||
| copyright notices, patent notices, disclaimers of warranty, or limitations | |||||
| of liability) contained within the Source Code Form of the Covered | |||||
| Software, except that You may alter any license notices to the extent | |||||
| required to remedy known factual inaccuracies. | |||||
| 3.5. Application of Additional Terms | |||||
| You may choose to offer, and to charge a fee for, warranty, support, | |||||
| indemnity or liability obligations to one or more recipients of Covered | |||||
| Software. However, You may do so only on Your own behalf, and not on behalf | |||||
| of any Contributor. You must make it absolutely clear that any such | |||||
| warranty, support, indemnity, or liability obligation is offered by You | |||||
| alone, and You hereby agree to indemnify every Contributor for any | |||||
| liability incurred by such Contributor as a result of warranty, support, | |||||
| indemnity or liability terms You offer. You may include additional | |||||
| disclaimers of warranty and limitations of liability specific to any | |||||
| jurisdiction. | |||||
| 4. Inability to Comply Due to Statute or Regulation | |||||
| If it is impossible for You to comply with any of the terms of this License | |||||
| with respect to some or all of the Covered Software due to statute, judicial | |||||
| order, or regulation then You must: (a) comply with the terms of this License | |||||
| to the maximum extent possible; and (b) describe the limitations and the code | |||||
| they affect. Such description must be placed in a text file included with all | |||||
| distributions of the Covered Software under this License. Except to the | |||||
| extent prohibited by statute or regulation, such description must be | |||||
| sufficiently detailed for a recipient of ordinary skill to be able to | |||||
| understand it. | |||||
| 5. Termination | |||||
| 5.1. The rights granted under this License will terminate automatically if You | |||||
| fail to comply with any of its terms. However, if You become compliant, | |||||
| then the rights granted under this License from a particular Contributor | |||||
| are reinstated (a) provisionally, unless and until such Contributor | |||||
| explicitly and finally terminates Your grants, and (b) on an ongoing basis, | |||||
| if such Contributor fails to notify You of the non-compliance by some | |||||
| reasonable means prior to 60 days after You have come back into compliance. | |||||
| Moreover, Your grants from a particular Contributor are reinstated on an | |||||
| ongoing basis if such Contributor notifies You of the non-compliance by | |||||
| some reasonable means, this is the first time You have received notice of | |||||
| non-compliance with this License from such Contributor, and You become | |||||
| compliant prior to 30 days after Your receipt of the notice. | |||||
| 5.2. If You initiate litigation against any entity by asserting a patent | |||||
| infringement claim (excluding declaratory judgment actions, counter-claims, | |||||
| and cross-claims) alleging that a Contributor Version directly or | |||||
| indirectly infringes any patent, then the rights granted to You by any and | |||||
| all Contributors for the Covered Software under Section 2.1 of this License | |||||
| shall terminate. | |||||
| 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user | |||||
| license agreements (excluding distributors and resellers) which have been | |||||
| validly granted by You or Your distributors under this License prior to | |||||
| termination shall survive termination. | |||||
| 6. Disclaimer of Warranty | |||||
| Covered Software is provided under this License on an “as is” basis, without | |||||
| warranty of any kind, either expressed, implied, or statutory, including, | |||||
| without limitation, warranties that the Covered Software is free of defects, | |||||
| merchantable, fit for a particular purpose or non-infringing. The entire | |||||
| risk as to the quality and performance of the Covered Software is with You. | |||||
| Should any Covered Software prove defective in any respect, You (not any | |||||
| Contributor) assume the cost of any necessary servicing, repair, or | |||||
| correction. This disclaimer of warranty constitutes an essential part of this | |||||
| License. No use of any Covered Software is authorized under this License | |||||
| except under this disclaimer. | |||||
| 7. Limitation of Liability | |||||
| Under no circumstances and under no legal theory, whether tort (including | |||||
| negligence), contract, or otherwise, shall any Contributor, or anyone who | |||||
| distributes Covered Software as permitted above, be liable to You for any | |||||
| direct, indirect, special, incidental, or consequential damages of any | |||||
| character including, without limitation, damages for lost profits, loss of | |||||
| goodwill, work stoppage, computer failure or malfunction, or any and all | |||||
| other commercial damages or losses, even if such party shall have been | |||||
| informed of the possibility of such damages. This limitation of liability | |||||
| shall not apply to liability for death or personal injury resulting from such | |||||
| party’s negligence to the extent applicable law prohibits such limitation. | |||||
| Some jurisdictions do not allow the exclusion or limitation of incidental or | |||||
| consequential damages, so this exclusion and limitation may not apply to You. | |||||
| 8. Litigation | |||||
| Any litigation relating to this License may be brought only in the courts of | |||||
| a jurisdiction where the defendant maintains its principal place of business | |||||
| and such litigation shall be governed by laws of that jurisdiction, without | |||||
| reference to its conflict-of-law provisions. Nothing in this Section shall | |||||
| prevent a party’s ability to bring cross-claims or counter-claims. | |||||
| 9. Miscellaneous | |||||
| This License represents the complete agreement concerning the subject matter | |||||
| hereof. If any provision of this License is held to be unenforceable, such | |||||
| provision shall be reformed only to the extent necessary to make it | |||||
| enforceable. Any law or regulation which provides that the language of a | |||||
| contract shall be construed against the drafter shall not be used to construe | |||||
| this License against a Contributor. | |||||
| 10. Versions of the License | |||||
| 10.1. New Versions | |||||
| Mozilla Foundation is the license steward. Except as provided in Section | |||||
| 10.3, no one other than the license steward has the right to modify or | |||||
| publish new versions of this License. Each version will be given a | |||||
| distinguishing version number. | |||||
| 10.2. Effect of New Versions | |||||
| You may distribute the Covered Software under the terms of the version of | |||||
| the License under which You originally received the Covered Software, or | |||||
| under the terms of any subsequent version published by the license | |||||
| steward. | |||||
| 10.3. Modified Versions | |||||
| If you create software not governed by this License, and you want to | |||||
| create a new license for such software, you may create and use a modified | |||||
| version of this License if you rename the license and remove any | |||||
| references to the name of the license steward (except to note that such | |||||
| modified license differs from this License). | |||||
| 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses | |||||
| If You choose to distribute Source Code Form that is Incompatible With | |||||
| Secondary Licenses under the terms of this version of the License, the | |||||
| notice described in Exhibit B of this License must be attached. | |||||
| Exhibit A - Source Code Form License Notice | |||||
| This Source Code Form is subject to the | |||||
| terms of the Mozilla Public License, v. | |||||
| 2.0. If a copy of the MPL was not | |||||
| distributed with this file, You can | |||||
| obtain one at | |||||
| http://mozilla.org/MPL/2.0/. | |||||
| If it is not possible or desirable to put the notice in a particular file, then | |||||
| You may include the notice in a location (such as a LICENSE file in a relevant | |||||
| directory) where a recipient would be likely to look for such a notice. | |||||
| You may add additional accurate notices of copyright ownership. | |||||
| Exhibit B - “Incompatible With Secondary Licenses” Notice | |||||
| This Source Code Form is “Incompatible | |||||
| With Secondary Licenses”, as defined by | |||||
| the Mozilla Public License, v. 2.0. | |||||
| @@ -0,0 +1,34 @@ | |||||
| .PHONY: update-deps install-deps fmt lint golint test test-with-coverage | |||||
| # TODO: When Go 1.9 is released vendor folder should be ignored automatically | |||||
| PACKAGES=`go list ./... | grep -v vendor | grep -v mocks` | |||||
| fmt: | |||||
| for pkg in ${PACKAGES}; do \ | |||||
| go fmt $$pkg; \ | |||||
| done; | |||||
| lint: | |||||
| gometalinter --exclude=vendor/ --tests --config=gometalinter.json --disable-all -E vet -E gofmt -E misspell -E ineffassign -E goimports -E deadcode ./... | |||||
| golint: | |||||
| for pkg in ${PACKAGES}; do \ | |||||
| golint $$pkg; \ | |||||
| done; | |||||
| test: | |||||
| TEST_FAILED= ; \ | |||||
| for pkg in ${PACKAGES}; do \ | |||||
| go test $$pkg || TEST_FAILED=1; \ | |||||
| done; \ | |||||
| [ -z "$$TEST_FAILED" ] | |||||
| test-with-coverage: | |||||
| echo "" > coverage.out | |||||
| echo "mode: set" > coverage-all.out | |||||
| TEST_FAILED= ; \ | |||||
| for pkg in ${PACKAGES}; do \ | |||||
| go test -coverprofile=coverage.out -covermode=set $$pkg || TEST_FAILED=1; \ | |||||
| tail -n +2 coverage.out >> coverage-all.out; \ | |||||
| done; \ | |||||
| [ -z "$$TEST_FAILED" ] | |||||
| #go tool cover -html=coverage-all.out | |||||
| @@ -0,0 +1,58 @@ | |||||
| ## Logging | |||||
| A simple leveled logging library with coloured output. | |||||
| [](https://travis-ci.org/RichardKnop/logging) | |||||
| [](http://godoc.org/github.com/RichardKnop/logging) | |||||
| [](https://codecov.io/gh/RichardKnop/logging) | |||||
| --- | |||||
| Log levels: | |||||
| - `INFO` (blue) | |||||
| - `WARNING` (pink) | |||||
| - `ERROR` (red) | |||||
| - `FATAL` (red) | |||||
| Formatters: | |||||
| - `DefaultFormatter` | |||||
| - `ColouredFormatter` | |||||
| Example usage. Create a new package `log` in your app such that: | |||||
| ```go | |||||
| package log | |||||
| import ( | |||||
| "github.com/RichardKnop/logging" | |||||
| ) | |||||
| var ( | |||||
| logger = logging.New(nil, nil, new(logging.ColouredFormatter)) | |||||
| // INFO ... | |||||
| INFO = logger[logging.INFO] | |||||
| // WARNING ... | |||||
| WARNING = logger[logging.WARNING] | |||||
| // ERROR ... | |||||
| ERROR = logger[logging.ERROR] | |||||
| // FATAL ... | |||||
| FATAL = logger[logging.FATAL] | |||||
| ) | |||||
| ``` | |||||
| Then from your app you could do: | |||||
| ```go | |||||
| package main | |||||
| import ( | |||||
| "github.com/yourusername/yourapp/log" | |||||
| ) | |||||
| func main() { | |||||
| log.INFO.Print("log message") | |||||
| } | |||||
| ``` | |||||
| @@ -0,0 +1,40 @@ | |||||
| package logging | |||||
| import ( | |||||
| "fmt" | |||||
| ) | |||||
| const ( | |||||
| // For colouring | |||||
| resetSeq = "\033[0m" | |||||
| colourSeq = "\033[0;%dm" | |||||
| ) | |||||
| // Colour map | |||||
| var colour = map[level]string{ | |||||
| INFO: fmt.Sprintf(colourSeq, 94), // blue | |||||
| WARNING: fmt.Sprintf(colourSeq, 95), // pink | |||||
| ERROR: fmt.Sprintf(colourSeq, 91), // red | |||||
| FATAL: fmt.Sprintf(colourSeq, 91), // red | |||||
| } | |||||
| // ColouredFormatter colours log messages with ASCI escape codes | |||||
| // and adds filename and line number before the log message | |||||
| // See https://en.wikipedia.org/wiki/ANSI_escape_code | |||||
| type ColouredFormatter struct { | |||||
| } | |||||
| // GetPrefix returns colour escape code | |||||
| func (f *ColouredFormatter) GetPrefix(lvl level) string { | |||||
| return colour[lvl] | |||||
| } | |||||
| // GetSuffix returns reset sequence code | |||||
| func (f *ColouredFormatter) GetSuffix(lvl level) string { | |||||
| return resetSeq | |||||
| } | |||||
| // Format adds filename and line number before the log message | |||||
| func (f *ColouredFormatter) Format(lvl level, v ...interface{}) []interface{} { | |||||
| return append([]interface{}{header()}, v...) | |||||
| } | |||||
| @@ -0,0 +1,20 @@ | |||||
| package logging | |||||
| // DefaultFormatter adds filename and line number before the log message | |||||
| type DefaultFormatter struct { | |||||
| } | |||||
| // GetPrefix returns "" | |||||
| func (f *DefaultFormatter) GetPrefix(lvl level) string { | |||||
| return "" | |||||
| } | |||||
| // GetSuffix returns "" | |||||
| func (f *DefaultFormatter) GetSuffix(lvl level) string { | |||||
| return "" | |||||
| } | |||||
| // Format adds filename and line number before the log message | |||||
| func (f *DefaultFormatter) Format(lvl level, v ...interface{}) []interface{} { | |||||
| return append([]interface{}{header()}, v...) | |||||
| } | |||||
| @@ -0,0 +1,30 @@ | |||||
| package logging | |||||
| import ( | |||||
| "fmt" | |||||
| "path/filepath" | |||||
| "runtime" | |||||
| ) | |||||
| const ( | |||||
| // Runtime caller depth | |||||
| depth = 3 | |||||
| ) | |||||
| // Formatter interface | |||||
| type Formatter interface { | |||||
| GetPrefix(lvl level) string | |||||
| Format(lvl level, v ...interface{}) []interface{} | |||||
| GetSuffix(lvl level) string | |||||
| } | |||||
| // Returns header including filename and line number | |||||
| func header() string { | |||||
| _, fn, line, ok := runtime.Caller(depth) | |||||
| if !ok { | |||||
| fn = "???" | |||||
| line = 1 | |||||
| } | |||||
| return fmt.Sprintf("%s:%d ", filepath.Base(fn), line) | |||||
| } | |||||
| @@ -0,0 +1,7 @@ | |||||
| module github.com/RichardKnop/logging | |||||
| require ( | |||||
| github.com/davecgh/go-spew v1.1.1 // indirect | |||||
| github.com/pmezard/go-difflib v1.0.0 // indirect | |||||
| github.com/stretchr/testify v1.2.2 | |||||
| ) | |||||
| @@ -0,0 +1,6 @@ | |||||
| github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | |||||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | |||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | |||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | |||||
| github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= | |||||
| github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= | |||||
| @@ -0,0 +1,9 @@ | |||||
| { | |||||
| "Linters": | |||||
| { | |||||
| "vet": | |||||
| { | |||||
| "Command": "go tool vet" | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,17 @@ | |||||
| package logging | |||||
| // LoggerInterface will accept stdlib logger and a custom logger. | |||||
| // There's no standard interface, this is the closest we get, unfortunately. | |||||
| type LoggerInterface interface { | |||||
| Print(...interface{}) | |||||
| Printf(string, ...interface{}) | |||||
| Println(...interface{}) | |||||
| Fatal(...interface{}) | |||||
| Fatalf(string, ...interface{}) | |||||
| Fatalln(...interface{}) | |||||
| Panic(...interface{}) | |||||
| Panicf(string, ...interface{}) | |||||
| Panicln(...interface{}) | |||||
| } | |||||
| @@ -0,0 +1,134 @@ | |||||
| package logging | |||||
| import ( | |||||
| "io" | |||||
| "log" | |||||
| "os" | |||||
| ) | |||||
| // Level type | |||||
| type level int | |||||
| const ( | |||||
| // DEBUG level | |||||
| DEBUG level = iota | |||||
| // INFO level | |||||
| INFO | |||||
| // WARNING level | |||||
| WARNING | |||||
| // ERROR level | |||||
| ERROR | |||||
| // FATAL level | |||||
| FATAL | |||||
| flag = log.Ldate | log.Ltime | |||||
| ) | |||||
| // Log level prefix map | |||||
| var prefix = map[level]string{ | |||||
| DEBUG: "DEBUG: ", | |||||
| INFO: "INFO: ", | |||||
| WARNING: "WARNING: ", | |||||
| ERROR: "ERROR: ", | |||||
| FATAL: "FATAL: ", | |||||
| } | |||||
| // Logger ... | |||||
| type Logger map[level]LoggerInterface | |||||
| // New returns instance of Logger | |||||
| func New(out, errOut io.Writer, f Formatter) Logger { | |||||
| // Fall back to stdout if out not set | |||||
| if out == nil { | |||||
| out = os.Stdout | |||||
| } | |||||
| // Fall back to stderr if errOut not set | |||||
| if errOut == nil { | |||||
| errOut = os.Stderr | |||||
| } | |||||
| // Fall back to DefaultFormatter if f not set | |||||
| if f == nil { | |||||
| f = new(DefaultFormatter) | |||||
| } | |||||
| l := make(map[level]LoggerInterface, 5) | |||||
| l[DEBUG] = &Wrapper{lvl: DEBUG, formatter: f, logger: log.New(out, f.GetPrefix(DEBUG)+prefix[DEBUG], flag)} | |||||
| l[INFO] = &Wrapper{lvl: INFO, formatter: f, logger: log.New(out, f.GetPrefix(INFO)+prefix[INFO], flag)} | |||||
| l[WARNING] = &Wrapper{lvl: INFO, formatter: f, logger: log.New(out, f.GetPrefix(WARNING)+prefix[WARNING], flag)} | |||||
| l[ERROR] = &Wrapper{lvl: INFO, formatter: f, logger: log.New(errOut, f.GetPrefix(ERROR)+prefix[ERROR], flag)} | |||||
| l[FATAL] = &Wrapper{lvl: INFO, formatter: f, logger: log.New(errOut, f.GetPrefix(FATAL)+prefix[FATAL], flag)} | |||||
| return Logger(l) | |||||
| } | |||||
| // Wrapper ... | |||||
| type Wrapper struct { | |||||
| lvl level | |||||
| formatter Formatter | |||||
| logger LoggerInterface | |||||
| } | |||||
| // Print ... | |||||
| func (w *Wrapper) Print(v ...interface{}) { | |||||
| v = w.formatter.Format(w.lvl, v...) | |||||
| v = append(v, w.formatter.GetSuffix(w.lvl)) | |||||
| w.logger.Print(v...) | |||||
| } | |||||
| // Printf ... | |||||
| func (w *Wrapper) Printf(format string, v ...interface{}) { | |||||
| suffix := w.formatter.GetSuffix(w.lvl) | |||||
| v = w.formatter.Format(w.lvl, v...) | |||||
| w.logger.Printf("%s"+format+suffix, v...) | |||||
| } | |||||
| // Println ... | |||||
| func (w *Wrapper) Println(v ...interface{}) { | |||||
| v = w.formatter.Format(w.lvl, v...) | |||||
| v = append(v, w.formatter.GetSuffix(w.lvl)) | |||||
| w.logger.Println(v...) | |||||
| } | |||||
| // Fatal ... | |||||
| func (w *Wrapper) Fatal(v ...interface{}) { | |||||
| v = w.formatter.Format(w.lvl, v...) | |||||
| v = append(v, w.formatter.GetSuffix(w.lvl)) | |||||
| w.logger.Fatal(v...) | |||||
| } | |||||
| // Fatalf ... | |||||
| func (w *Wrapper) Fatalf(format string, v ...interface{}) { | |||||
| suffix := w.formatter.GetSuffix(w.lvl) | |||||
| v = w.formatter.Format(w.lvl, v...) | |||||
| w.logger.Fatalf("%s"+format+suffix, v...) | |||||
| } | |||||
| // Fatalln ... | |||||
| func (w *Wrapper) Fatalln(v ...interface{}) { | |||||
| v = w.formatter.Format(w.lvl, v...) | |||||
| v = append(v, w.formatter.GetSuffix(w.lvl)) | |||||
| w.logger.Fatalln(v...) | |||||
| } | |||||
| // Panic ... | |||||
| func (w *Wrapper) Panic(v ...interface{}) { | |||||
| v = w.formatter.Format(w.lvl, v...) | |||||
| v = append(v, w.formatter.GetSuffix(w.lvl)) | |||||
| w.logger.Fatal(v...) | |||||
| } | |||||
| // Panicf ... | |||||
| func (w *Wrapper) Panicf(format string, v ...interface{}) { | |||||
| suffix := w.formatter.GetSuffix(w.lvl) | |||||
| v = w.formatter.Format(w.lvl, v...) | |||||
| w.logger.Panicf("%s"+format+suffix, v...) | |||||
| } | |||||
| // Panicln ... | |||||
| func (w *Wrapper) Panicln(v ...interface{}) { | |||||
| v = w.formatter.Format(w.lvl, v...) | |||||
| v = append(v, w.formatter.GetSuffix(w.lvl)) | |||||
| w.logger.Panicln(v...) | |||||
| } | |||||
| @@ -0,0 +1,354 @@ | |||||
| Mozilla Public License, version 2.0 | |||||
| 1. Definitions | |||||
| 1.1. “Contributor” | |||||
| means each individual or legal entity that creates, contributes to the | |||||
| creation of, or owns Covered Software. | |||||
| 1.2. “Contributor Version” | |||||
| means the combination of the Contributions of others (if any) used by a | |||||
| Contributor and that particular Contributor’s Contribution. | |||||
| 1.3. “Contribution” | |||||
| means Covered Software of a particular Contributor. | |||||
| 1.4. “Covered Software” | |||||
| means Source Code Form to which the initial Contributor has attached the | |||||
| notice in Exhibit A, the Executable Form of such Source Code Form, and | |||||
| Modifications of such Source Code Form, in each case including portions | |||||
| thereof. | |||||
| 1.5. “Incompatible With Secondary Licenses” | |||||
| means | |||||
| a. that the initial Contributor has attached the notice described in | |||||
| Exhibit B to the Covered Software; or | |||||
| b. that the Covered Software was made available under the terms of version | |||||
| 1.1 or earlier of the License, but not also under the terms of a | |||||
| Secondary License. | |||||
| 1.6. “Executable Form” | |||||
| means any form of the work other than Source Code Form. | |||||
| 1.7. “Larger Work” | |||||
| means a work that combines Covered Software with other material, in a separate | |||||
| file or files, that is not Covered Software. | |||||
| 1.8. “License” | |||||
| means this document. | |||||
| 1.9. “Licensable” | |||||
| means having the right to grant, to the maximum extent possible, whether at the | |||||
| time of the initial grant or subsequently, any and all of the rights conveyed by | |||||
| this License. | |||||
| 1.10. “Modifications” | |||||
| means any of the following: | |||||
| a. any file in Source Code Form that results from an addition to, deletion | |||||
| from, or modification of the contents of Covered Software; or | |||||
| b. any new file in Source Code Form that contains any Covered Software. | |||||
| 1.11. “Patent Claims” of a Contributor | |||||
| means any patent claim(s), including without limitation, method, process, | |||||
| and apparatus claims, in any patent Licensable by such Contributor that | |||||
| would be infringed, but for the grant of the License, by the making, | |||||
| using, selling, offering for sale, having made, import, or transfer of | |||||
| either its Contributions or its Contributor Version. | |||||
| 1.12. “Secondary License” | |||||
| means either the GNU General Public License, Version 2.0, the GNU Lesser | |||||
| General Public License, Version 2.1, the GNU Affero General Public | |||||
| License, Version 3.0, or any later versions of those licenses. | |||||
| 1.13. “Source Code Form” | |||||
| means the form of the work preferred for making modifications. | |||||
| 1.14. “You” (or “Your”) | |||||
| means an individual or a legal entity exercising rights under this | |||||
| License. For legal entities, “You” includes any entity that controls, is | |||||
| controlled by, or is under common control with You. For purposes of this | |||||
| definition, “control” means (a) the power, direct or indirect, to cause | |||||
| the direction or management of such entity, whether by contract or | |||||
| otherwise, or (b) ownership of more than fifty percent (50%) of the | |||||
| outstanding shares or beneficial ownership of such entity. | |||||
| 2. License Grants and Conditions | |||||
| 2.1. Grants | |||||
| Each Contributor hereby grants You a world-wide, royalty-free, | |||||
| non-exclusive license: | |||||
| a. under intellectual property rights (other than patent or trademark) | |||||
| Licensable by such Contributor to use, reproduce, make available, | |||||
| modify, display, perform, distribute, and otherwise exploit its | |||||
| Contributions, either on an unmodified basis, with Modifications, or as | |||||
| part of a Larger Work; and | |||||
| b. under Patent Claims of such Contributor to make, use, sell, offer for | |||||
| sale, have made, import, and otherwise transfer either its Contributions | |||||
| or its Contributor Version. | |||||
| 2.2. Effective Date | |||||
| The licenses granted in Section 2.1 with respect to any Contribution become | |||||
| effective for each Contribution on the date the Contributor first distributes | |||||
| such Contribution. | |||||
| 2.3. Limitations on Grant Scope | |||||
| The licenses granted in this Section 2 are the only rights granted under this | |||||
| License. No additional rights or licenses will be implied from the distribution | |||||
| or licensing of Covered Software under this License. Notwithstanding Section | |||||
| 2.1(b) above, no patent license is granted by a Contributor: | |||||
| a. for any code that a Contributor has removed from Covered Software; or | |||||
| b. for infringements caused by: (i) Your and any other third party’s | |||||
| modifications of Covered Software, or (ii) the combination of its | |||||
| Contributions with other software (except as part of its Contributor | |||||
| Version); or | |||||
| c. under Patent Claims infringed by Covered Software in the absence of its | |||||
| Contributions. | |||||
| This License does not grant any rights in the trademarks, service marks, or | |||||
| logos of any Contributor (except as may be necessary to comply with the | |||||
| notice requirements in Section 3.4). | |||||
| 2.4. Subsequent Licenses | |||||
| No Contributor makes additional grants as a result of Your choice to | |||||
| distribute the Covered Software under a subsequent version of this License | |||||
| (see Section 10.2) or under the terms of a Secondary License (if permitted | |||||
| under the terms of Section 3.3). | |||||
| 2.5. Representation | |||||
| Each Contributor represents that the Contributor believes its Contributions | |||||
| are its original creation(s) or it has sufficient rights to grant the | |||||
| rights to its Contributions conveyed by this License. | |||||
| 2.6. Fair Use | |||||
| This License is not intended to limit any rights You have under applicable | |||||
| copyright doctrines of fair use, fair dealing, or other equivalents. | |||||
| 2.7. Conditions | |||||
| Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in | |||||
| Section 2.1. | |||||
| 3. Responsibilities | |||||
| 3.1. Distribution of Source Form | |||||
| All distribution of Covered Software in Source Code Form, including any | |||||
| Modifications that You create or to which You contribute, must be under the | |||||
| terms of this License. You must inform recipients that the Source Code Form | |||||
| of the Covered Software is governed by the terms of this License, and how | |||||
| they can obtain a copy of this License. You may not attempt to alter or | |||||
| restrict the recipients’ rights in the Source Code Form. | |||||
| 3.2. Distribution of Executable Form | |||||
| If You distribute Covered Software in Executable Form then: | |||||
| a. such Covered Software must also be made available in Source Code Form, | |||||
| as described in Section 3.1, and You must inform recipients of the | |||||
| Executable Form how they can obtain a copy of such Source Code Form by | |||||
| reasonable means in a timely manner, at a charge no more than the cost | |||||
| of distribution to the recipient; and | |||||
| b. You may distribute such Executable Form under the terms of this License, | |||||
| or sublicense it under different terms, provided that the license for | |||||
| the Executable Form does not attempt to limit or alter the recipients’ | |||||
| rights in the Source Code Form under this License. | |||||
| 3.3. Distribution of a Larger Work | |||||
| You may create and distribute a Larger Work under terms of Your choice, | |||||
| provided that You also comply with the requirements of this License for the | |||||
| Covered Software. If the Larger Work is a combination of Covered Software | |||||
| with a work governed by one or more Secondary Licenses, and the Covered | |||||
| Software is not Incompatible With Secondary Licenses, this License permits | |||||
| You to additionally distribute such Covered Software under the terms of | |||||
| such Secondary License(s), so that the recipient of the Larger Work may, at | |||||
| their option, further distribute the Covered Software under the terms of | |||||
| either this License or such Secondary License(s). | |||||
| 3.4. Notices | |||||
| You may not remove or alter the substance of any license notices (including | |||||
| copyright notices, patent notices, disclaimers of warranty, or limitations | |||||
| of liability) contained within the Source Code Form of the Covered | |||||
| Software, except that You may alter any license notices to the extent | |||||
| required to remedy known factual inaccuracies. | |||||
| 3.5. Application of Additional Terms | |||||
| You may choose to offer, and to charge a fee for, warranty, support, | |||||
| indemnity or liability obligations to one or more recipients of Covered | |||||
| Software. However, You may do so only on Your own behalf, and not on behalf | |||||
| of any Contributor. You must make it absolutely clear that any such | |||||
| warranty, support, indemnity, or liability obligation is offered by You | |||||
| alone, and You hereby agree to indemnify every Contributor for any | |||||
| liability incurred by such Contributor as a result of warranty, support, | |||||
| indemnity or liability terms You offer. You may include additional | |||||
| disclaimers of warranty and limitations of liability specific to any | |||||
| jurisdiction. | |||||
| 4. Inability to Comply Due to Statute or Regulation | |||||
| If it is impossible for You to comply with any of the terms of this License | |||||
| with respect to some or all of the Covered Software due to statute, judicial | |||||
| order, or regulation then You must: (a) comply with the terms of this License | |||||
| to the maximum extent possible; and (b) describe the limitations and the code | |||||
| they affect. Such description must be placed in a text file included with all | |||||
| distributions of the Covered Software under this License. Except to the | |||||
| extent prohibited by statute or regulation, such description must be | |||||
| sufficiently detailed for a recipient of ordinary skill to be able to | |||||
| understand it. | |||||
| 5. Termination | |||||
| 5.1. The rights granted under this License will terminate automatically if You | |||||
| fail to comply with any of its terms. However, if You become compliant, | |||||
| then the rights granted under this License from a particular Contributor | |||||
| are reinstated (a) provisionally, unless and until such Contributor | |||||
| explicitly and finally terminates Your grants, and (b) on an ongoing basis, | |||||
| if such Contributor fails to notify You of the non-compliance by some | |||||
| reasonable means prior to 60 days after You have come back into compliance. | |||||
| Moreover, Your grants from a particular Contributor are reinstated on an | |||||
| ongoing basis if such Contributor notifies You of the non-compliance by | |||||
| some reasonable means, this is the first time You have received notice of | |||||
| non-compliance with this License from such Contributor, and You become | |||||
| compliant prior to 30 days after Your receipt of the notice. | |||||
| 5.2. If You initiate litigation against any entity by asserting a patent | |||||
| infringement claim (excluding declaratory judgment actions, counter-claims, | |||||
| and cross-claims) alleging that a Contributor Version directly or | |||||
| indirectly infringes any patent, then the rights granted to You by any and | |||||
| all Contributors for the Covered Software under Section 2.1 of this License | |||||
| shall terminate. | |||||
| 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user | |||||
| license agreements (excluding distributors and resellers) which have been | |||||
| validly granted by You or Your distributors under this License prior to | |||||
| termination shall survive termination. | |||||
| 6. Disclaimer of Warranty | |||||
| Covered Software is provided under this License on an “as is” basis, without | |||||
| warranty of any kind, either expressed, implied, or statutory, including, | |||||
| without limitation, warranties that the Covered Software is free of defects, | |||||
| merchantable, fit for a particular purpose or non-infringing. The entire | |||||
| risk as to the quality and performance of the Covered Software is with You. | |||||
| Should any Covered Software prove defective in any respect, You (not any | |||||
| Contributor) assume the cost of any necessary servicing, repair, or | |||||
| correction. This disclaimer of warranty constitutes an essential part of this | |||||
| License. No use of any Covered Software is authorized under this License | |||||
| except under this disclaimer. | |||||
| 7. Limitation of Liability | |||||
| Under no circumstances and under no legal theory, whether tort (including | |||||
| negligence), contract, or otherwise, shall any Contributor, or anyone who | |||||
| distributes Covered Software as permitted above, be liable to You for any | |||||
| direct, indirect, special, incidental, or consequential damages of any | |||||
| character including, without limitation, damages for lost profits, loss of | |||||
| goodwill, work stoppage, computer failure or malfunction, or any and all | |||||
| other commercial damages or losses, even if such party shall have been | |||||
| informed of the possibility of such damages. This limitation of liability | |||||
| shall not apply to liability for death or personal injury resulting from such | |||||
| party’s negligence to the extent applicable law prohibits such limitation. | |||||
| Some jurisdictions do not allow the exclusion or limitation of incidental or | |||||
| consequential damages, so this exclusion and limitation may not apply to You. | |||||
| 8. Litigation | |||||
| Any litigation relating to this License may be brought only in the courts of | |||||
| a jurisdiction where the defendant maintains its principal place of business | |||||
| and such litigation shall be governed by laws of that jurisdiction, without | |||||
| reference to its conflict-of-law provisions. Nothing in this Section shall | |||||
| prevent a party’s ability to bring cross-claims or counter-claims. | |||||
| 9. Miscellaneous | |||||
| This License represents the complete agreement concerning the subject matter | |||||
| hereof. If any provision of this License is held to be unenforceable, such | |||||
| provision shall be reformed only to the extent necessary to make it | |||||
| enforceable. Any law or regulation which provides that the language of a | |||||
| contract shall be construed against the drafter shall not be used to construe | |||||
| this License against a Contributor. | |||||
| 10. Versions of the License | |||||
| 10.1. New Versions | |||||
| Mozilla Foundation is the license steward. Except as provided in Section | |||||
| 10.3, no one other than the license steward has the right to modify or | |||||
| publish new versions of this License. Each version will be given a | |||||
| distinguishing version number. | |||||
| 10.2. Effect of New Versions | |||||
| You may distribute the Covered Software under the terms of the version of | |||||
| the License under which You originally received the Covered Software, or | |||||
| under the terms of any subsequent version published by the license | |||||
| steward. | |||||
| 10.3. Modified Versions | |||||
| If you create software not governed by this License, and you want to | |||||
| create a new license for such software, you may create and use a modified | |||||
| version of this License if you rename the license and remove any | |||||
| references to the name of the license steward (except to note that such | |||||
| modified license differs from this License). | |||||
| 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses | |||||
| If You choose to distribute Source Code Form that is Incompatible With | |||||
| Secondary Licenses under the terms of this version of the License, the | |||||
| notice described in Exhibit B of this License must be attached. | |||||
| Exhibit A - Source Code Form License Notice | |||||
| This Source Code Form is subject to the | |||||
| terms of the Mozilla Public License, v. | |||||
| 2.0. If a copy of the MPL was not | |||||
| distributed with this file, You can | |||||
| obtain one at | |||||
| http://mozilla.org/MPL/2.0/. | |||||
| If it is not possible or desirable to put the notice in a particular file, then | |||||
| You may include the notice in a location (such as a LICENSE file in a relevant | |||||
| directory) where a recipient would be likely to look for such a notice. | |||||
| You may add additional accurate notices of copyright ownership. | |||||
| Exhibit B - “Incompatible With Secondary Licenses” Notice | |||||
| This Source Code Form is “Incompatible | |||||
| With Secondary Licenses”, as defined by | |||||
| the Mozilla Public License, v. 2.0. | |||||
| @@ -0,0 +1,393 @@ | |||||
| package amqp | |||||
| // NOTE: Using AMQP as a result backend is quite tricky since every time we | |||||
| // read a message from the queue keeping task states, the message is removed | |||||
| // from the queue. This leads to problems with keeping a reliable state of a | |||||
| // group of tasks since concurrent processes updating the group state cause | |||||
| // race conditions and inconsistent state. | |||||
| // | |||||
| // This is avoided by a "clever" hack. A special queue identified by a group | |||||
| // UUID is created and we store serialised TaskState objects of successfully | |||||
| // completed tasks. By inspecting the queue we can then say: | |||||
| // 1) If all group tasks finished (number of unacked messages = group task count) | |||||
| // 2) If all group tasks finished AND succeeded (by consuming the queue) | |||||
| // | |||||
| // It is important to consume the queue exclusively to avoid race conditions. | |||||
| import ( | |||||
| "bytes" | |||||
| "encoding/json" | |||||
| "errors" | |||||
| "fmt" | |||||
| "github.com/RichardKnop/machinery/v1/backends/iface" | |||||
| "github.com/RichardKnop/machinery/v1/common" | |||||
| "github.com/RichardKnop/machinery/v1/config" | |||||
| "github.com/RichardKnop/machinery/v1/log" | |||||
| "github.com/RichardKnop/machinery/v1/tasks" | |||||
| "github.com/streadway/amqp" | |||||
| ) | |||||
| // Backend represents an AMQP result backend | |||||
| type Backend struct { | |||||
| common.Backend | |||||
| common.AMQPConnector | |||||
| } | |||||
| // New creates Backend instance | |||||
| func New(cnf *config.Config) iface.Backend { | |||||
| return &Backend{Backend: common.NewBackend(cnf), AMQPConnector: common.AMQPConnector{}} | |||||
| } | |||||
| // InitGroup creates and saves a group meta data object | |||||
| func (b *Backend) InitGroup(groupUUID string, taskUUIDs []string) error { | |||||
| return nil | |||||
| } | |||||
| // GroupCompleted returns true if all tasks in a group finished | |||||
| // NOTE: Given AMQP limitation this will only return true if all finished | |||||
| // tasks were successful as we do not keep track of completed failed tasks | |||||
| func (b *Backend) GroupCompleted(groupUUID string, groupTaskCount int) (bool, error) { | |||||
| conn, channel, err := b.Open(b.GetConfig().ResultBackend, b.GetConfig().TLSConfig) | |||||
| if err != nil { | |||||
| return false, err | |||||
| } | |||||
| defer b.Close(channel, conn) | |||||
| queueState, err := b.InspectQueue(channel, groupUUID) | |||||
| if err != nil { | |||||
| return false, nil | |||||
| } | |||||
| return queueState.Messages == groupTaskCount, nil | |||||
| } | |||||
| // GroupTaskStates returns states of all tasks in the group | |||||
| func (b *Backend) GroupTaskStates(groupUUID string, groupTaskCount int) ([]*tasks.TaskState, error) { | |||||
| conn, channel, err := b.Open(b.GetConfig().ResultBackend, b.GetConfig().TLSConfig) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| defer b.Close(channel, conn) | |||||
| queueState, err := b.InspectQueue(channel, groupUUID) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| if queueState.Messages != groupTaskCount { | |||||
| return nil, fmt.Errorf("Already consumed: %v", err) | |||||
| } | |||||
| deliveries, err := channel.Consume( | |||||
| groupUUID, // queue name | |||||
| "", // consumer tag | |||||
| false, // auto-ack | |||||
| true, // exclusive | |||||
| false, // no-local | |||||
| false, // no-wait | |||||
| nil, // arguments | |||||
| ) | |||||
| if err != nil { | |||||
| return nil, fmt.Errorf("Queue consume error: %s", err) | |||||
| } | |||||
| states := make([]*tasks.TaskState, groupTaskCount) | |||||
| for i := 0; i < groupTaskCount; i++ { | |||||
| d := <-deliveries | |||||
| state := new(tasks.TaskState) | |||||
| decoder := json.NewDecoder(bytes.NewReader([]byte(d.Body))) | |||||
| decoder.UseNumber() | |||||
| if err := decoder.Decode(state); err != nil { | |||||
| d.Nack(false, false) // multiple, requeue | |||||
| return nil, err | |||||
| } | |||||
| d.Ack(false) // multiple | |||||
| states[i] = state | |||||
| } | |||||
| return states, nil | |||||
| } | |||||
| // TriggerChord flags chord as triggered in the backend storage to make sure | |||||
| // chord is never trigerred multiple times. Returns a boolean flag to indicate | |||||
| // whether the worker should trigger chord (true) or no if it has been triggered | |||||
| // already (false) | |||||
| func (b *Backend) TriggerChord(groupUUID string) (bool, error) { | |||||
| conn, channel, err := b.Open(b.GetConfig().ResultBackend, b.GetConfig().TLSConfig) | |||||
| if err != nil { | |||||
| return false, err | |||||
| } | |||||
| defer b.Close(channel, conn) | |||||
| _, err = b.InspectQueue(channel, amqmChordTriggeredQueue(groupUUID)) | |||||
| if err != nil { | |||||
| return true, nil | |||||
| } | |||||
| return false, nil | |||||
| } | |||||
| // SetStatePending updates task state to PENDING | |||||
| func (b *Backend) SetStatePending(signature *tasks.Signature) error { | |||||
| taskState := tasks.NewPendingTaskState(signature) | |||||
| return b.updateState(taskState) | |||||
| } | |||||
| // SetStateReceived updates task state to RECEIVED | |||||
| func (b *Backend) SetStateReceived(signature *tasks.Signature) error { | |||||
| taskState := tasks.NewReceivedTaskState(signature) | |||||
| return b.updateState(taskState) | |||||
| } | |||||
| // SetStateStarted updates task state to STARTED | |||||
| func (b *Backend) SetStateStarted(signature *tasks.Signature) error { | |||||
| taskState := tasks.NewStartedTaskState(signature) | |||||
| return b.updateState(taskState) | |||||
| } | |||||
| // SetStateRetry updates task state to RETRY | |||||
| func (b *Backend) SetStateRetry(signature *tasks.Signature) error { | |||||
| state := tasks.NewRetryTaskState(signature) | |||||
| return b.updateState(state) | |||||
| } | |||||
| // SetStateSuccess updates task state to SUCCESS | |||||
| func (b *Backend) SetStateSuccess(signature *tasks.Signature, results []*tasks.TaskResult) error { | |||||
| taskState := tasks.NewSuccessTaskState(signature, results) | |||||
| if err := b.updateState(taskState); err != nil { | |||||
| return err | |||||
| } | |||||
| if signature.GroupUUID == "" { | |||||
| return nil | |||||
| } | |||||
| return b.markTaskCompleted(signature, taskState) | |||||
| } | |||||
| // SetStateFailure updates task state to FAILURE | |||||
| func (b *Backend) SetStateFailure(signature *tasks.Signature, err string) error { | |||||
| taskState := tasks.NewFailureTaskState(signature, err) | |||||
| if err := b.updateState(taskState); err != nil { | |||||
| return err | |||||
| } | |||||
| if signature.GroupUUID == "" { | |||||
| return nil | |||||
| } | |||||
| return b.markTaskCompleted(signature, taskState) | |||||
| } | |||||
| // GetState returns the latest task state. It will only return the status once | |||||
| // as the message will get consumed and removed from the queue. | |||||
| func (b *Backend) GetState(taskUUID string) (*tasks.TaskState, error) { | |||||
| declareQueueArgs := amqp.Table{ | |||||
| // Time in milliseconds | |||||
| // after that message will expire | |||||
| "x-message-ttl": int32(b.getExpiresIn()), | |||||
| // Time after that the queue will be deleted. | |||||
| "x-expires": int32(b.getExpiresIn()), | |||||
| } | |||||
| conn, channel, _, _, _, err := b.Connect( | |||||
| b.GetConfig().ResultBackend, | |||||
| b.GetConfig().TLSConfig, | |||||
| b.GetConfig().AMQP.Exchange, // exchange name | |||||
| b.GetConfig().AMQP.ExchangeType, // exchange type | |||||
| taskUUID, // queue name | |||||
| false, // queue durable | |||||
| true, // queue delete when unused | |||||
| taskUUID, // queue binding key | |||||
| nil, // exchange declare args | |||||
| declareQueueArgs, // queue declare args | |||||
| nil, // queue binding args | |||||
| ) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| defer b.Close(channel, conn) | |||||
| d, ok, err := channel.Get( | |||||
| taskUUID, // queue name | |||||
| false, // multiple | |||||
| ) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| if !ok { | |||||
| return nil, errors.New("No state ready") | |||||
| } | |||||
| d.Ack(false) | |||||
| state := new(tasks.TaskState) | |||||
| decoder := json.NewDecoder(bytes.NewReader([]byte(d.Body))) | |||||
| decoder.UseNumber() | |||||
| if err := decoder.Decode(state); err != nil { | |||||
| log.ERROR.Printf("Failed to unmarshal task state: %s", string(d.Body)) | |||||
| log.ERROR.Print(err) | |||||
| return nil, err | |||||
| } | |||||
| return state, nil | |||||
| } | |||||
| // PurgeState deletes stored task state | |||||
| func (b *Backend) PurgeState(taskUUID string) error { | |||||
| conn, channel, err := b.Open(b.GetConfig().ResultBackend, b.GetConfig().TLSConfig) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| defer b.Close(channel, conn) | |||||
| return b.DeleteQueue(channel, taskUUID) | |||||
| } | |||||
| // PurgeGroupMeta deletes stored group meta data | |||||
| func (b *Backend) PurgeGroupMeta(groupUUID string) error { | |||||
| conn, channel, err := b.Open(b.GetConfig().ResultBackend, b.GetConfig().TLSConfig) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| defer b.Close(channel, conn) | |||||
| b.DeleteQueue(channel, amqmChordTriggeredQueue(groupUUID)) | |||||
| return b.DeleteQueue(channel, groupUUID) | |||||
| } | |||||
| // updateState saves current task state | |||||
| func (b *Backend) updateState(taskState *tasks.TaskState) error { | |||||
| message, err := json.Marshal(taskState) | |||||
| if err != nil { | |||||
| return fmt.Errorf("JSON marshal error: %s", err) | |||||
| } | |||||
| declareQueueArgs := amqp.Table{ | |||||
| // Time in milliseconds | |||||
| // after that message will expire | |||||
| "x-message-ttl": int32(b.getExpiresIn()), | |||||
| // Time after that the queue will be deleted. | |||||
| "x-expires": int32(b.getExpiresIn()), | |||||
| } | |||||
| conn, channel, queue, confirmsChan, _, err := b.Connect( | |||||
| b.GetConfig().ResultBackend, | |||||
| b.GetConfig().TLSConfig, | |||||
| b.GetConfig().AMQP.Exchange, // exchange name | |||||
| b.GetConfig().AMQP.ExchangeType, // exchange type | |||||
| taskState.TaskUUID, // queue name | |||||
| false, // queue durable | |||||
| true, // queue delete when unused | |||||
| taskState.TaskUUID, // queue binding key | |||||
| nil, // exchange declare args | |||||
| declareQueueArgs, // queue declare args | |||||
| nil, // queue binding args | |||||
| ) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| defer b.Close(channel, conn) | |||||
| if err := channel.Publish( | |||||
| b.GetConfig().AMQP.Exchange, // exchange | |||||
| queue.Name, // routing key | |||||
| false, // mandatory | |||||
| false, // immediate | |||||
| amqp.Publishing{ | |||||
| ContentType: "application/json", | |||||
| Body: message, | |||||
| DeliveryMode: amqp.Persistent, // Persistent // Transient | |||||
| }, | |||||
| ); err != nil { | |||||
| return err | |||||
| } | |||||
| confirmed := <-confirmsChan | |||||
| if confirmed.Ack { | |||||
| return nil | |||||
| } | |||||
| return fmt.Errorf("Failed delivery of delivery tag: %d", confirmed.DeliveryTag) | |||||
| } | |||||
| // getExpiresIn returns expiration time | |||||
| func (b *Backend) getExpiresIn() int { | |||||
| resultsExpireIn := b.GetConfig().ResultsExpireIn * 1000 | |||||
| if resultsExpireIn == 0 { | |||||
| // // expire results after 1 hour by default | |||||
| resultsExpireIn = config.DefaultResultsExpireIn * 1000 | |||||
| } | |||||
| return resultsExpireIn | |||||
| } | |||||
| // markTaskCompleted marks task as completed in either groupdUUID_success | |||||
| // or groupUUID_failure queue. This is important for GroupCompleted and | |||||
| // GroupSuccessful methods | |||||
| func (b *Backend) markTaskCompleted(signature *tasks.Signature, taskState *tasks.TaskState) error { | |||||
| if signature.GroupUUID == "" || signature.GroupTaskCount == 0 { | |||||
| return nil | |||||
| } | |||||
| message, err := json.Marshal(taskState) | |||||
| if err != nil { | |||||
| return fmt.Errorf("JSON marshal error: %s", err) | |||||
| } | |||||
| declareQueueArgs := amqp.Table{ | |||||
| // Time in milliseconds | |||||
| // after that message will expire | |||||
| "x-message-ttl": int32(b.getExpiresIn()), | |||||
| // Time after that the queue will be deleted. | |||||
| "x-expires": int32(b.getExpiresIn()), | |||||
| } | |||||
| conn, channel, queue, confirmsChan, _, err := b.Connect( | |||||
| b.GetConfig().ResultBackend, | |||||
| b.GetConfig().TLSConfig, | |||||
| b.GetConfig().AMQP.Exchange, // exchange name | |||||
| b.GetConfig().AMQP.ExchangeType, // exchange type | |||||
| signature.GroupUUID, // queue name | |||||
| false, // queue durable | |||||
| true, // queue delete when unused | |||||
| signature.GroupUUID, // queue binding key | |||||
| nil, // exchange declare args | |||||
| declareQueueArgs, // queue declare args | |||||
| nil, // queue binding args | |||||
| ) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| defer b.Close(channel, conn) | |||||
| if err := channel.Publish( | |||||
| b.GetConfig().AMQP.Exchange, // exchange | |||||
| queue.Name, // routing key | |||||
| false, // mandatory | |||||
| false, // immediate | |||||
| amqp.Publishing{ | |||||
| ContentType: "application/json", | |||||
| Body: message, | |||||
| DeliveryMode: amqp.Persistent, // Persistent // Transient | |||||
| }, | |||||
| ); err != nil { | |||||
| return err | |||||
| } | |||||
| confirmed := <-confirmsChan | |||||
| if !confirmed.Ack { | |||||
| return fmt.Errorf("Failed delivery of delivery tag: %v", confirmed.DeliveryTag) | |||||
| } | |||||
| return nil | |||||
| } | |||||
| func amqmChordTriggeredQueue(groupUUID string) string { | |||||
| return fmt.Sprintf("%s_chord_triggered", groupUUID) | |||||
| } | |||||
| @@ -0,0 +1,512 @@ | |||||
| package dynamodb | |||||
| import ( | |||||
| "errors" | |||||
| "fmt" | |||||
| "time" | |||||
| "github.com/aws/aws-sdk-go/aws/session" | |||||
| "github.com/RichardKnop/machinery/v1/backends/iface" | |||||
| "github.com/RichardKnop/machinery/v1/common" | |||||
| "github.com/RichardKnop/machinery/v1/config" | |||||
| "github.com/RichardKnop/machinery/v1/log" | |||||
| "github.com/RichardKnop/machinery/v1/tasks" | |||||
| "github.com/aws/aws-sdk-go/aws" | |||||
| "github.com/aws/aws-sdk-go/service/dynamodb" | |||||
| "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" | |||||
| "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface" | |||||
| ) | |||||
| // Backend ... | |||||
| type Backend struct { | |||||
| common.Backend | |||||
| cnf *config.Config | |||||
| client dynamodbiface.DynamoDBAPI | |||||
| } | |||||
| // New creates a Backend instance | |||||
| func New(cnf *config.Config) iface.Backend { | |||||
| backend := &Backend{Backend: common.NewBackend(cnf), cnf: cnf} | |||||
| if cnf.DynamoDB != nil && cnf.DynamoDB.Client != nil { | |||||
| backend.client = cnf.DynamoDB.Client | |||||
| } else { | |||||
| sess := session.Must(session.NewSessionWithOptions(session.Options{ | |||||
| SharedConfigState: session.SharedConfigEnable, | |||||
| })) | |||||
| backend.client = dynamodb.New(sess) | |||||
| } | |||||
| // Check if needed tables exist | |||||
| err := backend.checkRequiredTablesIfExist() | |||||
| if err != nil { | |||||
| log.FATAL.Printf("Failed to prepare tables. Error: %v", err) | |||||
| } | |||||
| return backend | |||||
| } | |||||
| // InitGroup ... | |||||
| func (b *Backend) InitGroup(groupUUID string, taskUUIDs []string) error { | |||||
| meta := tasks.GroupMeta{ | |||||
| GroupUUID: groupUUID, | |||||
| TaskUUIDs: taskUUIDs, | |||||
| CreatedAt: time.Now().UTC(), | |||||
| } | |||||
| av, err := dynamodbattribute.MarshalMap(meta) | |||||
| if err != nil { | |||||
| log.ERROR.Printf("Error when marshaling Dynamodb attributes. Err: %v", err) | |||||
| return err | |||||
| } | |||||
| input := &dynamodb.PutItemInput{ | |||||
| Item: av, | |||||
| TableName: aws.String(b.cnf.DynamoDB.GroupMetasTable), | |||||
| } | |||||
| _, err = b.client.PutItem(input) | |||||
| if err != nil { | |||||
| log.ERROR.Printf("Got error when calling PutItem: %v; Error: %v", input, err) | |||||
| return err | |||||
| } | |||||
| return nil | |||||
| } | |||||
| // GroupCompleted ... | |||||
| func (b *Backend) GroupCompleted(groupUUID string, groupTaskCount int) (bool, error) { | |||||
| groupMeta, err := b.getGroupMeta(groupUUID) | |||||
| if err != nil { | |||||
| return false, err | |||||
| } | |||||
| taskStates, err := b.getStates(groupMeta.TaskUUIDs...) | |||||
| if err != nil { | |||||
| return false, err | |||||
| } | |||||
| var countSuccessTasks = 0 | |||||
| for _, taskState := range taskStates { | |||||
| if taskState.IsCompleted() { | |||||
| countSuccessTasks++ | |||||
| } | |||||
| } | |||||
| return countSuccessTasks == groupTaskCount, nil | |||||
| } | |||||
| // GroupTaskStates ... | |||||
| func (b *Backend) GroupTaskStates(groupUUID string, groupTaskCount int) ([]*tasks.TaskState, error) { | |||||
| groupMeta, err := b.getGroupMeta(groupUUID) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return b.getStates(groupMeta.TaskUUIDs...) | |||||
| } | |||||
| // TriggerChord ... | |||||
| func (b *Backend) TriggerChord(groupUUID string) (bool, error) { | |||||
| // Get the group meta data | |||||
| groupMeta, err := b.getGroupMeta(groupUUID) | |||||
| if err != nil { | |||||
| return false, err | |||||
| } | |||||
| // Chord has already been triggered, return false (should not trigger again) | |||||
| if groupMeta.ChordTriggered { | |||||
| return false, nil | |||||
| } | |||||
| // If group meta is locked, wait until it's unlocked | |||||
| for groupMeta.Lock { | |||||
| groupMeta, _ = b.getGroupMeta(groupUUID) | |||||
| log.WARNING.Print("Group meta locked, waiting") | |||||
| time.Sleep(time.Millisecond * 5) | |||||
| } | |||||
| // Acquire lock | |||||
| if err = b.lockGroupMeta(groupUUID); err != nil { | |||||
| return false, err | |||||
| } | |||||
| defer b.unlockGroupMeta(groupUUID) | |||||
| // update group meta data | |||||
| err = b.chordTriggered(groupUUID) | |||||
| if err != nil { | |||||
| return false, err | |||||
| } | |||||
| return true, err | |||||
| } | |||||
| // SetStatePending ... | |||||
| func (b *Backend) SetStatePending(signature *tasks.Signature) error { | |||||
| taskState := tasks.NewPendingTaskState(signature) | |||||
| // taskUUID is the primary key of the table, so a new task need to be created first, instead of using dynamodb.UpdateItemInput directly | |||||
| return b.initTaskState(taskState) | |||||
| } | |||||
| // SetStateReceived ... | |||||
| func (b *Backend) SetStateReceived(signature *tasks.Signature) error { | |||||
| taskState := tasks.NewReceivedTaskState(signature) | |||||
| return b.setTaskState(taskState) | |||||
| } | |||||
| // SetStateStarted ... | |||||
| func (b *Backend) SetStateStarted(signature *tasks.Signature) error { | |||||
| taskState := tasks.NewStartedTaskState(signature) | |||||
| return b.setTaskState(taskState) | |||||
| } | |||||
| // SetStateRetry ... | |||||
| func (b *Backend) SetStateRetry(signature *tasks.Signature) error { | |||||
| taskState := tasks.NewRetryTaskState(signature) | |||||
| return b.setTaskState(taskState) | |||||
| } | |||||
| // SetStateSuccess ... | |||||
| func (b *Backend) SetStateSuccess(signature *tasks.Signature, results []*tasks.TaskResult) error { | |||||
| taskState := tasks.NewSuccessTaskState(signature, results) | |||||
| return b.setTaskState(taskState) | |||||
| } | |||||
| // SetStateFailure ... | |||||
| func (b *Backend) SetStateFailure(signature *tasks.Signature, err string) error { | |||||
| taskState := tasks.NewFailureTaskState(signature, err) | |||||
| return b.updateToFailureStateWithError(taskState) | |||||
| } | |||||
| // GetState ... | |||||
| func (b *Backend) GetState(taskUUID string) (*tasks.TaskState, error) { | |||||
| result, err := b.client.GetItem(&dynamodb.GetItemInput{ | |||||
| TableName: aws.String(b.cnf.DynamoDB.TaskStatesTable), | |||||
| Key: map[string]*dynamodb.AttributeValue{ | |||||
| "TaskUUID": { | |||||
| S: aws.String(taskUUID), | |||||
| }, | |||||
| }, | |||||
| }) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return b.unmarshalTaskStateGetItemResult(result) | |||||
| } | |||||
| // PurgeState ... | |||||
| func (b *Backend) PurgeState(taskUUID string) error { | |||||
| input := &dynamodb.DeleteItemInput{ | |||||
| Key: map[string]*dynamodb.AttributeValue{ | |||||
| "TaskUUID": { | |||||
| N: aws.String(taskUUID), | |||||
| }, | |||||
| }, | |||||
| TableName: aws.String(b.cnf.DynamoDB.TaskStatesTable), | |||||
| } | |||||
| _, err := b.client.DeleteItem(input) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| return nil | |||||
| } | |||||
| // PurgeGroupMeta ... | |||||
| func (b *Backend) PurgeGroupMeta(groupUUID string) error { | |||||
| input := &dynamodb.DeleteItemInput{ | |||||
| Key: map[string]*dynamodb.AttributeValue{ | |||||
| "GroupUUID": { | |||||
| N: aws.String(groupUUID), | |||||
| }, | |||||
| }, | |||||
| TableName: aws.String(b.cnf.DynamoDB.GroupMetasTable), | |||||
| } | |||||
| _, err := b.client.DeleteItem(input) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| return nil | |||||
| } | |||||
| func (b *Backend) getGroupMeta(groupUUID string) (*tasks.GroupMeta, error) { | |||||
| result, err := b.client.GetItem(&dynamodb.GetItemInput{ | |||||
| TableName: aws.String(b.cnf.DynamoDB.GroupMetasTable), | |||||
| Key: map[string]*dynamodb.AttributeValue{ | |||||
| "GroupUUID": { | |||||
| S: aws.String(groupUUID), | |||||
| }, | |||||
| }, | |||||
| }) | |||||
| if err != nil { | |||||
| log.ERROR.Printf("Error when getting group meta. Error: %v", err) | |||||
| return nil, err | |||||
| } | |||||
| item, err := b.unmarshalGroupMetaGetItemResult(result) | |||||
| if err != nil { | |||||
| log.INFO.Println("!!!", result) | |||||
| log.ERROR.Printf("Failed to unmarshal item, %v", err) | |||||
| return nil, err | |||||
| } | |||||
| return item, nil | |||||
| } | |||||
| func (b *Backend) getStates(taskUUIDs ...string) ([]*tasks.TaskState, error) { | |||||
| var states []*tasks.TaskState | |||||
| stateChan := make(chan *tasks.TaskState, len(taskUUIDs)) | |||||
| errChan := make(chan error) | |||||
| // There is no method like querying items by `in` a list of primary keys. | |||||
| // So a for loop with go routine is used to get multiple items | |||||
| for _, id := range taskUUIDs { | |||||
| go func(id string) { | |||||
| state, err := b.GetState(id) | |||||
| if err != nil { | |||||
| errChan <- err | |||||
| } | |||||
| stateChan <- state | |||||
| }(id) | |||||
| } | |||||
| for s := range stateChan { | |||||
| states = append(states, s) | |||||
| if len(states) == len(taskUUIDs) { | |||||
| close(stateChan) | |||||
| } | |||||
| } | |||||
| return states, nil | |||||
| } | |||||
| func (b *Backend) lockGroupMeta(groupUUID string) error { | |||||
| err := b.updateGroupMetaLock(groupUUID, true) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| return nil | |||||
| } | |||||
| func (b *Backend) unlockGroupMeta(groupUUID string) error { | |||||
| err := b.updateGroupMetaLock(groupUUID, false) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| return nil | |||||
| } | |||||
| func (b *Backend) updateGroupMetaLock(groupUUID string, status bool) error { | |||||
| input := &dynamodb.UpdateItemInput{ | |||||
| ExpressionAttributeNames: map[string]*string{ | |||||
| "#L": aws.String("Lock"), | |||||
| }, | |||||
| ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ | |||||
| ":l": { | |||||
| BOOL: aws.Bool(status), | |||||
| }, | |||||
| }, | |||||
| Key: map[string]*dynamodb.AttributeValue{ | |||||
| "GroupUUID": { | |||||
| S: aws.String(groupUUID), | |||||
| }, | |||||
| }, | |||||
| ReturnValues: aws.String("UPDATED_NEW"), | |||||
| TableName: aws.String(b.cnf.DynamoDB.GroupMetasTable), | |||||
| UpdateExpression: aws.String("SET #L = :l"), | |||||
| } | |||||
| _, err := b.client.UpdateItem(input) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| return nil | |||||
| } | |||||
| func (b *Backend) chordTriggered(groupUUID string) error { | |||||
| input := &dynamodb.UpdateItemInput{ | |||||
| ExpressionAttributeNames: map[string]*string{ | |||||
| "#CT": aws.String("ChordTriggered"), | |||||
| }, | |||||
| ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ | |||||
| ":ct": { | |||||
| BOOL: aws.Bool(true), | |||||
| }, | |||||
| }, | |||||
| Key: map[string]*dynamodb.AttributeValue{ | |||||
| "GroupUUID": { | |||||
| S: aws.String(groupUUID), | |||||
| }, | |||||
| }, | |||||
| ReturnValues: aws.String("UPDATED_NEW"), | |||||
| TableName: aws.String(b.cnf.DynamoDB.GroupMetasTable), | |||||
| UpdateExpression: aws.String("SET #CT = :ct"), | |||||
| } | |||||
| _, err := b.client.UpdateItem(input) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| return nil | |||||
| } | |||||
| func (b *Backend) setTaskState(taskState *tasks.TaskState) error { | |||||
| expAttributeNames := map[string]*string{ | |||||
| "#S": aws.String("State"), | |||||
| } | |||||
| expAttributeValues := map[string]*dynamodb.AttributeValue{ | |||||
| ":s": { | |||||
| S: aws.String(taskState.State), | |||||
| }, | |||||
| } | |||||
| keyAttributeValues := map[string]*dynamodb.AttributeValue{ | |||||
| "TaskUUID": { | |||||
| S: aws.String(taskState.TaskUUID), | |||||
| }, | |||||
| } | |||||
| exp := "SET #S = :s" | |||||
| if !taskState.CreatedAt.IsZero() { | |||||
| expAttributeNames["#C"] = aws.String("CreatedAt") | |||||
| expAttributeValues[":c"] = &dynamodb.AttributeValue{ | |||||
| S: aws.String(taskState.CreatedAt.String()), | |||||
| } | |||||
| exp += ", #C = :c" | |||||
| } | |||||
| if taskState.Results != nil && len(taskState.Results) != 0 { | |||||
| expAttributeNames["#R"] = aws.String("Results") | |||||
| var results []*dynamodb.AttributeValue | |||||
| for _, r := range taskState.Results { | |||||
| avMap := map[string]*dynamodb.AttributeValue{ | |||||
| "Type": { | |||||
| S: aws.String(r.Type), | |||||
| }, | |||||
| "Value": { | |||||
| S: aws.String(fmt.Sprintf("%v", r.Value)), | |||||
| }, | |||||
| } | |||||
| rs := &dynamodb.AttributeValue{ | |||||
| M: avMap, | |||||
| } | |||||
| results = append(results, rs) | |||||
| } | |||||
| expAttributeValues[":r"] = &dynamodb.AttributeValue{ | |||||
| L: results, | |||||
| } | |||||
| exp += ", #R = :r" | |||||
| } | |||||
| input := &dynamodb.UpdateItemInput{ | |||||
| ExpressionAttributeNames: expAttributeNames, | |||||
| ExpressionAttributeValues: expAttributeValues, | |||||
| Key: keyAttributeValues, | |||||
| ReturnValues: aws.String("UPDATED_NEW"), | |||||
| TableName: aws.String(b.cnf.DynamoDB.TaskStatesTable), | |||||
| UpdateExpression: aws.String(exp), | |||||
| } | |||||
| _, err := b.client.UpdateItem(input) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| return nil | |||||
| } | |||||
| func (b *Backend) initTaskState(taskState *tasks.TaskState) error { | |||||
| av, err := dynamodbattribute.MarshalMap(taskState) | |||||
| input := &dynamodb.PutItemInput{ | |||||
| Item: av, | |||||
| TableName: aws.String(b.cnf.DynamoDB.TaskStatesTable), | |||||
| } | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| _, err = b.client.PutItem(input) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| return nil | |||||
| } | |||||
| func (b *Backend) updateToFailureStateWithError(taskState *tasks.TaskState) error { | |||||
| input := &dynamodb.UpdateItemInput{ | |||||
| ExpressionAttributeNames: map[string]*string{ | |||||
| "#S": aws.String("State"), | |||||
| "#E": aws.String("Error"), | |||||
| }, | |||||
| ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ | |||||
| ":s": { | |||||
| S: aws.String(taskState.State), | |||||
| }, | |||||
| ":e": { | |||||
| S: aws.String(taskState.Error), | |||||
| }, | |||||
| }, | |||||
| Key: map[string]*dynamodb.AttributeValue{ | |||||
| "TaskUUID": { | |||||
| S: aws.String(taskState.TaskUUID), | |||||
| }, | |||||
| }, | |||||
| ReturnValues: aws.String("UPDATED_NEW"), | |||||
| TableName: aws.String(b.cnf.DynamoDB.TaskStatesTable), | |||||
| UpdateExpression: aws.String("SET #S = :s, #E = :e"), | |||||
| } | |||||
| _, err := b.client.UpdateItem(input) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| return nil | |||||
| } | |||||
| func (b *Backend) unmarshalGroupMetaGetItemResult(result *dynamodb.GetItemOutput) (*tasks.GroupMeta, error) { | |||||
| if result == nil { | |||||
| err := errors.New("task state is nil") | |||||
| log.ERROR.Printf("Got error when unmarshal map. Error: %v", err) | |||||
| return nil, err | |||||
| } | |||||
| item := tasks.GroupMeta{} | |||||
| err := dynamodbattribute.UnmarshalMap(result.Item, &item) | |||||
| if err != nil { | |||||
| log.ERROR.Printf("Got error when unmarshal map. Error: %v", err) | |||||
| return nil, err | |||||
| } | |||||
| return &item, err | |||||
| } | |||||
| func (b *Backend) unmarshalTaskStateGetItemResult(result *dynamodb.GetItemOutput) (*tasks.TaskState, error) { | |||||
| if result == nil { | |||||
| err := errors.New("task state is nil") | |||||
| log.ERROR.Printf("Got error when unmarshal map. Error: %v", err) | |||||
| return nil, err | |||||
| } | |||||
| state := tasks.TaskState{} | |||||
| err := dynamodbattribute.UnmarshalMap(result.Item, &state) | |||||
| if err != nil { | |||||
| log.ERROR.Printf("Got error when unmarshal map. Error: %v", err) | |||||
| return nil, err | |||||
| } | |||||
| return &state, nil | |||||
| } | |||||
| func (b *Backend) checkRequiredTablesIfExist() error { | |||||
| var ( | |||||
| taskTableName = b.cnf.DynamoDB.TaskStatesTable | |||||
| groupTableName = b.cnf.DynamoDB.GroupMetasTable | |||||
| ) | |||||
| result, err := b.client.ListTables(&dynamodb.ListTablesInput{}) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| if !b.tableExists(taskTableName, result.TableNames) { | |||||
| return errors.New("task table doesn't exist") | |||||
| } | |||||
| if !b.tableExists(groupTableName, result.TableNames) { | |||||
| return errors.New("group table doesn't exist") | |||||
| } | |||||
| return nil | |||||
| } | |||||
| func (b *Backend) tableExists(tableName string, tableNames []*string) bool { | |||||
| for _, t := range tableNames { | |||||
| if tableName == *t { | |||||
| return true | |||||
| } | |||||
| } | |||||
| return false | |||||
| } | |||||
| @@ -0,0 +1,210 @@ | |||||
| package eager | |||||
| import ( | |||||
| "bytes" | |||||
| "encoding/json" | |||||
| "fmt" | |||||
| "sync" | |||||
| "github.com/RichardKnop/machinery/v1/backends/iface" | |||||
| "github.com/RichardKnop/machinery/v1/common" | |||||
| "github.com/RichardKnop/machinery/v1/config" | |||||
| "github.com/RichardKnop/machinery/v1/tasks" | |||||
| ) | |||||
| // ErrGroupNotFound ... | |||||
| type ErrGroupNotFound struct { | |||||
| groupUUID string | |||||
| } | |||||
| // NewErrGroupNotFound returns new instance of ErrGroupNotFound | |||||
| func NewErrGroupNotFound(groupUUID string) ErrGroupNotFound { | |||||
| return ErrGroupNotFound{groupUUID: groupUUID} | |||||
| } | |||||
| // Error implements error interface | |||||
| func (e ErrGroupNotFound) Error() string { | |||||
| return fmt.Sprintf("Group not found: %v", e.groupUUID) | |||||
| } | |||||
| // ErrTasknotFound ... | |||||
| type ErrTasknotFound struct { | |||||
| taskUUID string | |||||
| } | |||||
| // NewErrTasknotFound returns new instance of ErrTasknotFound | |||||
| func NewErrTasknotFound(taskUUID string) ErrTasknotFound { | |||||
| return ErrTasknotFound{taskUUID: taskUUID} | |||||
| } | |||||
| // Error implements error interface | |||||
| func (e ErrTasknotFound) Error() string { | |||||
| return fmt.Sprintf("Task not found: %v", e.taskUUID) | |||||
| } | |||||
| // Backend represents an "eager" in-memory result backend | |||||
| type Backend struct { | |||||
| common.Backend | |||||
| groups map[string][]string | |||||
| tasks map[string][]byte | |||||
| stateMutex sync.Mutex | |||||
| } | |||||
| // New creates EagerBackend instance | |||||
| func New() iface.Backend { | |||||
| return &Backend{ | |||||
| Backend: common.NewBackend(new(config.Config)), | |||||
| groups: make(map[string][]string), | |||||
| tasks: make(map[string][]byte), | |||||
| } | |||||
| } | |||||
| // InitGroup creates and saves a group meta data object | |||||
| func (b *Backend) InitGroup(groupUUID string, taskUUIDs []string) error { | |||||
| tasks := make([]string, 0, len(taskUUIDs)) | |||||
| // copy every task | |||||
| for _, v := range taskUUIDs { | |||||
| tasks = append(tasks, v) | |||||
| } | |||||
| b.groups[groupUUID] = tasks | |||||
| return nil | |||||
| } | |||||
| // GroupCompleted returns true if all tasks in a group finished | |||||
| func (b *Backend) GroupCompleted(groupUUID string, groupTaskCount int) (bool, error) { | |||||
| tasks, ok := b.groups[groupUUID] | |||||
| if !ok { | |||||
| return false, NewErrGroupNotFound(groupUUID) | |||||
| } | |||||
| var countSuccessTasks = 0 | |||||
| for _, v := range tasks { | |||||
| t, err := b.GetState(v) | |||||
| if err != nil { | |||||
| return false, err | |||||
| } | |||||
| if t.IsCompleted() { | |||||
| countSuccessTasks++ | |||||
| } | |||||
| } | |||||
| return countSuccessTasks == groupTaskCount, nil | |||||
| } | |||||
| // GroupTaskStates returns states of all tasks in the group | |||||
| func (b *Backend) GroupTaskStates(groupUUID string, groupTaskCount int) ([]*tasks.TaskState, error) { | |||||
| taskUUIDs, ok := b.groups[groupUUID] | |||||
| if !ok { | |||||
| return nil, NewErrGroupNotFound(groupUUID) | |||||
| } | |||||
| ret := make([]*tasks.TaskState, 0, groupTaskCount) | |||||
| for _, taskUUID := range taskUUIDs { | |||||
| t, err := b.GetState(taskUUID) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| ret = append(ret, t) | |||||
| } | |||||
| return ret, nil | |||||
| } | |||||
| // TriggerChord flags chord as triggered in the backend storage to make sure | |||||
| // chord is never trigerred multiple times. Returns a boolean flag to indicate | |||||
| // whether the worker should trigger chord (true) or no if it has been triggered | |||||
| // already (false) | |||||
| func (b *Backend) TriggerChord(groupUUID string) (bool, error) { | |||||
| return true, nil | |||||
| } | |||||
| // SetStatePending updates task state to PENDING | |||||
| func (b *Backend) SetStatePending(signature *tasks.Signature) error { | |||||
| state := tasks.NewPendingTaskState(signature) | |||||
| return b.updateState(state) | |||||
| } | |||||
| // SetStateReceived updates task state to RECEIVED | |||||
| func (b *Backend) SetStateReceived(signature *tasks.Signature) error { | |||||
| state := tasks.NewReceivedTaskState(signature) | |||||
| return b.updateState(state) | |||||
| } | |||||
| // SetStateStarted updates task state to STARTED | |||||
| func (b *Backend) SetStateStarted(signature *tasks.Signature) error { | |||||
| state := tasks.NewStartedTaskState(signature) | |||||
| return b.updateState(state) | |||||
| } | |||||
| // SetStateRetry updates task state to RETRY | |||||
| func (b *Backend) SetStateRetry(signature *tasks.Signature) error { | |||||
| state := tasks.NewRetryTaskState(signature) | |||||
| return b.updateState(state) | |||||
| } | |||||
| // SetStateSuccess updates task state to SUCCESS | |||||
| func (b *Backend) SetStateSuccess(signature *tasks.Signature, results []*tasks.TaskResult) error { | |||||
| state := tasks.NewSuccessTaskState(signature, results) | |||||
| return b.updateState(state) | |||||
| } | |||||
| // SetStateFailure updates task state to FAILURE | |||||
| func (b *Backend) SetStateFailure(signature *tasks.Signature, err string) error { | |||||
| state := tasks.NewFailureTaskState(signature, err) | |||||
| return b.updateState(state) | |||||
| } | |||||
| // GetState returns the latest task state | |||||
| func (b *Backend) GetState(taskUUID string) (*tasks.TaskState, error) { | |||||
| tasktStateBytes, ok := b.tasks[taskUUID] | |||||
| if !ok { | |||||
| return nil, NewErrTasknotFound(taskUUID) | |||||
| } | |||||
| state := new(tasks.TaskState) | |||||
| decoder := json.NewDecoder(bytes.NewReader(tasktStateBytes)) | |||||
| decoder.UseNumber() | |||||
| if err := decoder.Decode(state); err != nil { | |||||
| return nil, fmt.Errorf("Failed to unmarshal task state %v", b) | |||||
| } | |||||
| return state, nil | |||||
| } | |||||
| // PurgeState deletes stored task state | |||||
| func (b *Backend) PurgeState(taskUUID string) error { | |||||
| _, ok := b.tasks[taskUUID] | |||||
| if !ok { | |||||
| return NewErrTasknotFound(taskUUID) | |||||
| } | |||||
| delete(b.tasks, taskUUID) | |||||
| return nil | |||||
| } | |||||
| // PurgeGroupMeta deletes stored group meta data | |||||
| func (b *Backend) PurgeGroupMeta(groupUUID string) error { | |||||
| _, ok := b.groups[groupUUID] | |||||
| if !ok { | |||||
| return NewErrGroupNotFound(groupUUID) | |||||
| } | |||||
| delete(b.groups, groupUUID) | |||||
| return nil | |||||
| } | |||||
| func (b *Backend) updateState(s *tasks.TaskState) error { | |||||
| // simulate the behavior of json marshal/unmarshal | |||||
| b.stateMutex.Lock() | |||||
| defer b.stateMutex.Unlock() | |||||
| msg, err := json.Marshal(s) | |||||
| if err != nil { | |||||
| return fmt.Errorf("Marshal task state error: %v", err) | |||||
| } | |||||
| b.tasks[s.TaskUUID] = msg | |||||
| return nil | |||||
| } | |||||
| @@ -0,0 +1,28 @@ | |||||
| package iface | |||||
| import ( | |||||
| "github.com/RichardKnop/machinery/v1/tasks" | |||||
| ) | |||||
| // Backend - a common interface for all result backends | |||||
| type Backend interface { | |||||
| // Group related functions | |||||
| InitGroup(groupUUID string, taskUUIDs []string) error | |||||
| GroupCompleted(groupUUID string, groupTaskCount int) (bool, error) | |||||
| GroupTaskStates(groupUUID string, groupTaskCount int) ([]*tasks.TaskState, error) | |||||
| TriggerChord(groupUUID string) (bool, error) | |||||
| // Setting / getting task state | |||||
| SetStatePending(signature *tasks.Signature) error | |||||
| SetStateReceived(signature *tasks.Signature) error | |||||
| SetStateStarted(signature *tasks.Signature) error | |||||
| SetStateRetry(signature *tasks.Signature) error | |||||
| SetStateSuccess(signature *tasks.Signature, results []*tasks.TaskResult) error | |||||
| SetStateFailure(signature *tasks.Signature, err string) error | |||||
| GetState(taskUUID string) (*tasks.TaskState, error) | |||||
| // Purging stored stored tasks states and group meta data | |||||
| IsAMQP() bool | |||||
| PurgeState(taskUUID string) error | |||||
| PurgeGroupMeta(groupUUID string) error | |||||
| } | |||||
| @@ -0,0 +1,292 @@ | |||||
| package memcache | |||||
| import ( | |||||
| "bytes" | |||||
| "encoding/json" | |||||
| "time" | |||||
| "github.com/RichardKnop/machinery/v1/backends/iface" | |||||
| "github.com/RichardKnop/machinery/v1/common" | |||||
| "github.com/RichardKnop/machinery/v1/config" | |||||
| "github.com/RichardKnop/machinery/v1/log" | |||||
| "github.com/RichardKnop/machinery/v1/tasks" | |||||
| gomemcache "github.com/bradfitz/gomemcache/memcache" | |||||
| ) | |||||
| // Backend represents a Memcache result backend | |||||
| type Backend struct { | |||||
| common.Backend | |||||
| servers []string | |||||
| client *gomemcache.Client | |||||
| } | |||||
| // New creates Backend instance | |||||
| func New(cnf *config.Config, servers []string) iface.Backend { | |||||
| return &Backend{ | |||||
| Backend: common.NewBackend(cnf), | |||||
| servers: servers, | |||||
| } | |||||
| } | |||||
| // InitGroup creates and saves a group meta data object | |||||
| func (b *Backend) InitGroup(groupUUID string, taskUUIDs []string) error { | |||||
| groupMeta := &tasks.GroupMeta{ | |||||
| GroupUUID: groupUUID, | |||||
| TaskUUIDs: taskUUIDs, | |||||
| CreatedAt: time.Now().UTC(), | |||||
| } | |||||
| encoded, err := json.Marshal(&groupMeta) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| return b.getClient().Set(&gomemcache.Item{ | |||||
| Key: groupUUID, | |||||
| Value: encoded, | |||||
| Expiration: b.getExpirationTimestamp(), | |||||
| }) | |||||
| } | |||||
| // GroupCompleted returns true if all tasks in a group finished | |||||
| func (b *Backend) GroupCompleted(groupUUID string, groupTaskCount int) (bool, error) { | |||||
| groupMeta, err := b.getGroupMeta(groupUUID) | |||||
| if err != nil { | |||||
| return false, err | |||||
| } | |||||
| taskStates, err := b.getStates(groupMeta.TaskUUIDs...) | |||||
| if err != nil { | |||||
| return false, err | |||||
| } | |||||
| var countSuccessTasks = 0 | |||||
| for _, taskState := range taskStates { | |||||
| if taskState.IsCompleted() { | |||||
| countSuccessTasks++ | |||||
| } | |||||
| } | |||||
| return countSuccessTasks == groupTaskCount, nil | |||||
| } | |||||
| // GroupTaskStates returns states of all tasks in the group | |||||
| func (b *Backend) GroupTaskStates(groupUUID string, groupTaskCount int) ([]*tasks.TaskState, error) { | |||||
| groupMeta, err := b.getGroupMeta(groupUUID) | |||||
| if err != nil { | |||||
| return []*tasks.TaskState{}, err | |||||
| } | |||||
| return b.getStates(groupMeta.TaskUUIDs...) | |||||
| } | |||||
| // TriggerChord flags chord as triggered in the backend storage to make sure | |||||
| // chord is never trigerred multiple times. Returns a boolean flag to indicate | |||||
| // whether the worker should trigger chord (true) or no if it has been triggered | |||||
| // already (false) | |||||
| func (b *Backend) TriggerChord(groupUUID string) (bool, error) { | |||||
| groupMeta, err := b.getGroupMeta(groupUUID) | |||||
| if err != nil { | |||||
| return false, err | |||||
| } | |||||
| // Chord has already been triggered, return false (should not trigger again) | |||||
| if groupMeta.ChordTriggered { | |||||
| return false, nil | |||||
| } | |||||
| // If group meta is locked, wait until it's unlocked | |||||
| for groupMeta.Lock { | |||||
| groupMeta, _ = b.getGroupMeta(groupUUID) | |||||
| log.WARNING.Print("Group meta locked, waiting") | |||||
| time.Sleep(time.Millisecond * 5) | |||||
| } | |||||
| // Acquire lock | |||||
| if err = b.lockGroupMeta(groupMeta); err != nil { | |||||
| return false, err | |||||
| } | |||||
| defer b.unlockGroupMeta(groupMeta) | |||||
| // Update the group meta data | |||||
| groupMeta.ChordTriggered = true | |||||
| encoded, err := json.Marshal(&groupMeta) | |||||
| if err != nil { | |||||
| return false, err | |||||
| } | |||||
| if err = b.getClient().Replace(&gomemcache.Item{ | |||||
| Key: groupUUID, | |||||
| Value: encoded, | |||||
| Expiration: b.getExpirationTimestamp(), | |||||
| }); err != nil { | |||||
| return false, err | |||||
| } | |||||
| return true, nil | |||||
| } | |||||
| // SetStatePending updates task state to PENDING | |||||
| func (b *Backend) SetStatePending(signature *tasks.Signature) error { | |||||
| taskState := tasks.NewPendingTaskState(signature) | |||||
| return b.updateState(taskState) | |||||
| } | |||||
| // SetStateReceived updates task state to RECEIVED | |||||
| func (b *Backend) SetStateReceived(signature *tasks.Signature) error { | |||||
| taskState := tasks.NewReceivedTaskState(signature) | |||||
| return b.updateState(taskState) | |||||
| } | |||||
| // SetStateStarted updates task state to STARTED | |||||
| func (b *Backend) SetStateStarted(signature *tasks.Signature) error { | |||||
| taskState := tasks.NewStartedTaskState(signature) | |||||
| return b.updateState(taskState) | |||||
| } | |||||
| // SetStateRetry updates task state to RETRY | |||||
| func (b *Backend) SetStateRetry(signature *tasks.Signature) error { | |||||
| state := tasks.NewRetryTaskState(signature) | |||||
| return b.updateState(state) | |||||
| } | |||||
| // SetStateSuccess updates task state to SUCCESS | |||||
| func (b *Backend) SetStateSuccess(signature *tasks.Signature, results []*tasks.TaskResult) error { | |||||
| taskState := tasks.NewSuccessTaskState(signature, results) | |||||
| return b.updateState(taskState) | |||||
| } | |||||
| // SetStateFailure updates task state to FAILURE | |||||
| func (b *Backend) SetStateFailure(signature *tasks.Signature, err string) error { | |||||
| taskState := tasks.NewFailureTaskState(signature, err) | |||||
| return b.updateState(taskState) | |||||
| } | |||||
| // GetState returns the latest task state | |||||
| func (b *Backend) GetState(taskUUID string) (*tasks.TaskState, error) { | |||||
| item, err := b.getClient().Get(taskUUID) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| state := new(tasks.TaskState) | |||||
| decoder := json.NewDecoder(bytes.NewReader(item.Value)) | |||||
| decoder.UseNumber() | |||||
| if err := decoder.Decode(state); err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return state, nil | |||||
| } | |||||
| // PurgeState deletes stored task state | |||||
| func (b *Backend) PurgeState(taskUUID string) error { | |||||
| return b.getClient().Delete(taskUUID) | |||||
| } | |||||
| // PurgeGroupMeta deletes stored group meta data | |||||
| func (b *Backend) PurgeGroupMeta(groupUUID string) error { | |||||
| return b.getClient().Delete(groupUUID) | |||||
| } | |||||
| // updateState saves current task state | |||||
| func (b *Backend) updateState(taskState *tasks.TaskState) error { | |||||
| encoded, err := json.Marshal(taskState) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| return b.getClient().Set(&gomemcache.Item{ | |||||
| Key: taskState.TaskUUID, | |||||
| Value: encoded, | |||||
| Expiration: b.getExpirationTimestamp(), | |||||
| }) | |||||
| } | |||||
| // lockGroupMeta acquires lock on group meta data | |||||
| func (b *Backend) lockGroupMeta(groupMeta *tasks.GroupMeta) error { | |||||
| groupMeta.Lock = true | |||||
| encoded, err := json.Marshal(groupMeta) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| return b.getClient().Set(&gomemcache.Item{ | |||||
| Key: groupMeta.GroupUUID, | |||||
| Value: encoded, | |||||
| Expiration: b.getExpirationTimestamp(), | |||||
| }) | |||||
| } | |||||
| // unlockGroupMeta releases lock on group meta data | |||||
| func (b *Backend) unlockGroupMeta(groupMeta *tasks.GroupMeta) error { | |||||
| groupMeta.Lock = false | |||||
| encoded, err := json.Marshal(groupMeta) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| return b.getClient().Set(&gomemcache.Item{ | |||||
| Key: groupMeta.GroupUUID, | |||||
| Value: encoded, | |||||
| Expiration: b.getExpirationTimestamp(), | |||||
| }) | |||||
| } | |||||
| // getGroupMeta retrieves group meta data, convenience function to avoid repetition | |||||
| func (b *Backend) getGroupMeta(groupUUID string) (*tasks.GroupMeta, error) { | |||||
| item, err := b.getClient().Get(groupUUID) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| groupMeta := new(tasks.GroupMeta) | |||||
| decoder := json.NewDecoder(bytes.NewReader(item.Value)) | |||||
| decoder.UseNumber() | |||||
| if err := decoder.Decode(groupMeta); err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return groupMeta, nil | |||||
| } | |||||
| // getStates returns multiple task states | |||||
| func (b *Backend) getStates(taskUUIDs ...string) ([]*tasks.TaskState, error) { | |||||
| states := make([]*tasks.TaskState, len(taskUUIDs)) | |||||
| for i, taskUUID := range taskUUIDs { | |||||
| item, err := b.getClient().Get(taskUUID) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| state := new(tasks.TaskState) | |||||
| decoder := json.NewDecoder(bytes.NewReader(item.Value)) | |||||
| decoder.UseNumber() | |||||
| if err := decoder.Decode(state); err != nil { | |||||
| return nil, err | |||||
| } | |||||
| states[i] = state | |||||
| } | |||||
| return states, nil | |||||
| } | |||||
| // getExpirationTimestamp returns expiration timestamp | |||||
| func (b *Backend) getExpirationTimestamp() int32 { | |||||
| expiresIn := b.GetConfig().ResultsExpireIn | |||||
| if expiresIn == 0 { | |||||
| // // expire results after 1 hour by default | |||||
| expiresIn = config.DefaultResultsExpireIn | |||||
| } | |||||
| return int32(time.Now().Unix() + int64(expiresIn)) | |||||
| } | |||||
| // getClient returns or creates instance of Memcache client | |||||
| func (b *Backend) getClient() *gomemcache.Client { | |||||
| if b.client == nil { | |||||
| b.client = gomemcache.New(b.servers...) | |||||
| } | |||||
| return b.client | |||||
| } | |||||
| @@ -0,0 +1,358 @@ | |||||
| package mongo | |||||
| import ( | |||||
| "context" | |||||
| "encoding/json" | |||||
| "fmt" | |||||
| "reflect" | |||||
| "strings" | |||||
| "sync" | |||||
| "time" | |||||
| "go.mongodb.org/mongo-driver/bson" | |||||
| "go.mongodb.org/mongo-driver/mongo" | |||||
| "go.mongodb.org/mongo-driver/mongo/options" | |||||
| "github.com/RichardKnop/machinery/v1/backends/iface" | |||||
| "github.com/RichardKnop/machinery/v1/common" | |||||
| "github.com/RichardKnop/machinery/v1/config" | |||||
| "github.com/RichardKnop/machinery/v1/log" | |||||
| "github.com/RichardKnop/machinery/v1/tasks" | |||||
| ) | |||||
| // Backend represents a MongoDB result backend | |||||
| type Backend struct { | |||||
| common.Backend | |||||
| client *mongo.Client | |||||
| tc *mongo.Collection | |||||
| gmc *mongo.Collection | |||||
| once sync.Once | |||||
| } | |||||
| // New creates Backend instance | |||||
| func New(cnf *config.Config) (iface.Backend, error) { | |||||
| backend := &Backend{ | |||||
| Backend: common.NewBackend(cnf), | |||||
| once: sync.Once{}, | |||||
| } | |||||
| return backend, nil | |||||
| } | |||||
| // InitGroup creates and saves a group meta data object | |||||
| func (b *Backend) InitGroup(groupUUID string, taskUUIDs []string) error { | |||||
| groupMeta := &tasks.GroupMeta{ | |||||
| GroupUUID: groupUUID, | |||||
| TaskUUIDs: taskUUIDs, | |||||
| CreatedAt: time.Now().UTC(), | |||||
| } | |||||
| _, err := b.groupMetasCollection().InsertOne(context.Background(), groupMeta) | |||||
| return err | |||||
| } | |||||
| // GroupCompleted returns true if all tasks in a group finished | |||||
| func (b *Backend) GroupCompleted(groupUUID string, groupTaskCount int) (bool, error) { | |||||
| groupMeta, err := b.getGroupMeta(groupUUID) | |||||
| if err != nil { | |||||
| return false, err | |||||
| } | |||||
| taskStates, err := b.getStates(groupMeta.TaskUUIDs...) | |||||
| if err != nil { | |||||
| return false, err | |||||
| } | |||||
| var countSuccessTasks = 0 | |||||
| for _, taskState := range taskStates { | |||||
| if taskState.IsCompleted() { | |||||
| countSuccessTasks++ | |||||
| } | |||||
| } | |||||
| return countSuccessTasks == groupTaskCount, nil | |||||
| } | |||||
| // GroupTaskStates returns states of all tasks in the group | |||||
| func (b *Backend) GroupTaskStates(groupUUID string, groupTaskCount int) ([]*tasks.TaskState, error) { | |||||
| groupMeta, err := b.getGroupMeta(groupUUID) | |||||
| if err != nil { | |||||
| return []*tasks.TaskState{}, err | |||||
| } | |||||
| return b.getStates(groupMeta.TaskUUIDs...) | |||||
| } | |||||
| // TriggerChord flags chord as triggered in the backend storage to make sure | |||||
| // chord is never triggered multiple times. Returns a boolean flag to indicate | |||||
| // whether the worker should trigger chord (true) or no if it has been triggered | |||||
| // already (false) | |||||
| func (b *Backend) TriggerChord(groupUUID string) (bool, error) { | |||||
| query := bson.M{ | |||||
| "_id": groupUUID, | |||||
| "chord_triggered": false, | |||||
| } | |||||
| change := bson.M{ | |||||
| "$set": bson.M{ | |||||
| "chord_triggered": true, | |||||
| }, | |||||
| } | |||||
| _, err := b.groupMetasCollection().UpdateOne(context.Background(), query, change, options.Update()) | |||||
| if err != nil { | |||||
| if err == mongo.ErrNoDocuments { | |||||
| log.WARNING.Printf("Chord already triggered for group %s", groupUUID) | |||||
| return false, nil | |||||
| } | |||||
| return false, err | |||||
| } | |||||
| return true, nil | |||||
| } | |||||
| // SetStatePending updates task state to PENDING | |||||
| func (b *Backend) SetStatePending(signature *tasks.Signature) error { | |||||
| update := bson.M{ | |||||
| "state": tasks.StatePending, | |||||
| "task_name": signature.Name, | |||||
| "created_at": time.Now().UTC(), | |||||
| } | |||||
| return b.updateState(signature, update) | |||||
| } | |||||
| // SetStateReceived updates task state to RECEIVED | |||||
| func (b *Backend) SetStateReceived(signature *tasks.Signature) error { | |||||
| update := bson.M{"state": tasks.StateReceived} | |||||
| return b.updateState(signature, update) | |||||
| } | |||||
| // SetStateStarted updates task state to STARTED | |||||
| func (b *Backend) SetStateStarted(signature *tasks.Signature) error { | |||||
| update := bson.M{"state": tasks.StateStarted} | |||||
| return b.updateState(signature, update) | |||||
| } | |||||
| // SetStateRetry updates task state to RETRY | |||||
| func (b *Backend) SetStateRetry(signature *tasks.Signature) error { | |||||
| update := bson.M{"state": tasks.StateRetry} | |||||
| return b.updateState(signature, update) | |||||
| } | |||||
| // SetStateSuccess updates task state to SUCCESS | |||||
| func (b *Backend) SetStateSuccess(signature *tasks.Signature, results []*tasks.TaskResult) error { | |||||
| decodedResults := b.decodeResults(results) | |||||
| update := bson.M{ | |||||
| "state": tasks.StateSuccess, | |||||
| "results": decodedResults, | |||||
| } | |||||
| return b.updateState(signature, update) | |||||
| } | |||||
| // decodeResults detects & decodes json strings in TaskResult.Value and returns a new slice | |||||
| func (b *Backend) decodeResults(results []*tasks.TaskResult) []*tasks.TaskResult { | |||||
| l := len(results) | |||||
| jsonResults := make([]*tasks.TaskResult, l, l) | |||||
| for i, result := range results { | |||||
| jsonResult := new(bson.M) | |||||
| resultType := reflect.TypeOf(result.Value).Kind() | |||||
| if resultType == reflect.String { | |||||
| err := json.NewDecoder(strings.NewReader(result.Value.(string))).Decode(&jsonResult) | |||||
| if err == nil { | |||||
| jsonResults[i] = &tasks.TaskResult{ | |||||
| Type: "json", | |||||
| Value: jsonResult, | |||||
| } | |||||
| continue | |||||
| } | |||||
| } | |||||
| jsonResults[i] = result | |||||
| } | |||||
| return jsonResults | |||||
| } | |||||
| // SetStateFailure updates task state to FAILURE | |||||
| func (b *Backend) SetStateFailure(signature *tasks.Signature, err string) error { | |||||
| update := bson.M{"state": tasks.StateFailure, "error": err} | |||||
| return b.updateState(signature, update) | |||||
| } | |||||
| // GetState returns the latest task state | |||||
| func (b *Backend) GetState(taskUUID string) (*tasks.TaskState, error) { | |||||
| state := &tasks.TaskState{} | |||||
| err := b.tasksCollection().FindOne(context.Background(), bson.M{"_id": taskUUID}).Decode(state) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return state, nil | |||||
| } | |||||
| // PurgeState deletes stored task state | |||||
| func (b *Backend) PurgeState(taskUUID string) error { | |||||
| _, err := b.tasksCollection().DeleteOne(context.Background(), bson.M{"_id": taskUUID}) | |||||
| return err | |||||
| } | |||||
| // PurgeGroupMeta deletes stored group meta data | |||||
| func (b *Backend) PurgeGroupMeta(groupUUID string) error { | |||||
| _, err := b.groupMetasCollection().DeleteOne(context.Background(), bson.M{"_id": groupUUID}) | |||||
| return err | |||||
| } | |||||
| // lockGroupMeta acquires lock on groupUUID document | |||||
| func (b *Backend) lockGroupMeta(groupUUID string) error { | |||||
| query := bson.M{ | |||||
| "_id": groupUUID, | |||||
| "lock": false, | |||||
| } | |||||
| change := bson.M{ | |||||
| "$set": bson.M{ | |||||
| "lock": true, | |||||
| }, | |||||
| } | |||||
| _, err := b.groupMetasCollection().UpdateOne(context.Background(), query, change, options.Update().SetUpsert(true)) | |||||
| return err | |||||
| } | |||||
| // unlockGroupMeta releases lock on groupUUID document | |||||
| func (b *Backend) unlockGroupMeta(groupUUID string) error { | |||||
| update := bson.M{"$set": bson.M{"lock": false}} | |||||
| _, err := b.groupMetasCollection().UpdateOne(context.Background(), bson.M{"_id": groupUUID}, update, options.Update()) | |||||
| return err | |||||
| } | |||||
| // getGroupMeta retrieves group meta data, convenience function to avoid repetition | |||||
| func (b *Backend) getGroupMeta(groupUUID string) (*tasks.GroupMeta, error) { | |||||
| groupMeta := &tasks.GroupMeta{} | |||||
| query := bson.M{"_id": groupUUID} | |||||
| err := b.groupMetasCollection().FindOne(context.Background(), query).Decode(groupMeta) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return groupMeta, nil | |||||
| } | |||||
| // getStates returns multiple task states | |||||
| func (b *Backend) getStates(taskUUIDs ...string) ([]*tasks.TaskState, error) { | |||||
| states := make([]*tasks.TaskState, 0, len(taskUUIDs)) | |||||
| cur, err := b.tasksCollection().Find(context.Background(), bson.M{"_id": bson.M{"$in": taskUUIDs}}) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| defer cur.Close(context.Background()) | |||||
| for cur.Next(context.Background()) { | |||||
| state := &tasks.TaskState{} | |||||
| if err := cur.Decode(state); err != nil { | |||||
| return nil, err | |||||
| } | |||||
| states = append(states, state) | |||||
| } | |||||
| if cur.Err() != nil { | |||||
| return nil, err | |||||
| } | |||||
| return states, nil | |||||
| } | |||||
| // updateState saves current task state | |||||
| func (b *Backend) updateState(signature *tasks.Signature, update bson.M) error { | |||||
| update = bson.M{"$set": update} | |||||
| _, err := b.tasksCollection().UpdateOne(context.Background(), bson.M{"_id": signature.UUID}, update, options.Update().SetUpsert(true)) | |||||
| return err | |||||
| } | |||||
| func (b *Backend) tasksCollection() *mongo.Collection { | |||||
| b.once.Do(func() { | |||||
| b.connect() | |||||
| }) | |||||
| return b.tc | |||||
| } | |||||
| func (b *Backend) groupMetasCollection() *mongo.Collection { | |||||
| b.once.Do(func() { | |||||
| b.connect() | |||||
| }) | |||||
| return b.gmc | |||||
| } | |||||
| // connect creates the underlying mgo connection if it doesn't exist | |||||
| // creates required indexes for our collections | |||||
| func (b *Backend) connect() error { | |||||
| client, err := b.dial() | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| b.client = client | |||||
| database := "machinery" | |||||
| if b.GetConfig().MongoDB != nil { | |||||
| database = b.GetConfig().MongoDB.Database | |||||
| } | |||||
| b.tc = b.client.Database(database).Collection("tasks") | |||||
| b.gmc = b.client.Database(database).Collection("group_metas") | |||||
| err = b.createMongoIndexes(database) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| return nil | |||||
| } | |||||
| // dial connects to mongo with TLSConfig if provided | |||||
| // else connects via ResultBackend uri | |||||
| func (b *Backend) dial() (*mongo.Client, error) { | |||||
| if b.GetConfig().MongoDB != nil && b.GetConfig().MongoDB.Client != nil { | |||||
| return b.GetConfig().MongoDB.Client, nil | |||||
| } | |||||
| uri := b.GetConfig().ResultBackend | |||||
| if strings.HasPrefix(uri, "mongodb://") == false && | |||||
| strings.HasPrefix(uri, "mongodb+srv://") == false { | |||||
| uri = fmt.Sprintf("mongodb://%s", uri) | |||||
| } | |||||
| client, err := mongo.NewClient(options.Client().ApplyURI(uri)) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) | |||||
| defer cancel() | |||||
| if err := client.Connect(ctx); err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return client, nil | |||||
| } | |||||
| // createMongoIndexes ensures all indexes are in place | |||||
| func (b *Backend) createMongoIndexes(database string) error { | |||||
| tasksCollection := b.client.Database(database).Collection("tasks") | |||||
| expireIn := int32(b.GetConfig().ResultsExpireIn) | |||||
| _, err := tasksCollection.Indexes().CreateMany(context.Background(), []mongo.IndexModel{ | |||||
| { | |||||
| Keys: bson.M{"state": 1}, | |||||
| Options: options.Index().SetBackground(true).SetExpireAfterSeconds(expireIn), | |||||
| }, | |||||
| mongo.IndexModel{ | |||||
| Keys: bson.M{"lock": 1}, | |||||
| Options: options.Index().SetBackground(true).SetExpireAfterSeconds(expireIn), | |||||
| }, | |||||
| }) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| return err | |||||
| } | |||||
| @@ -0,0 +1,150 @@ | |||||
| package null | |||||
| import ( | |||||
| "fmt" | |||||
| "github.com/RichardKnop/machinery/v1/backends/iface" | |||||
| "github.com/RichardKnop/machinery/v1/common" | |||||
| "github.com/RichardKnop/machinery/v1/config" | |||||
| "github.com/RichardKnop/machinery/v1/tasks" | |||||
| ) | |||||
| // ErrGroupNotFound ... | |||||
| type ErrGroupNotFound struct { | |||||
| groupUUID string | |||||
| } | |||||
| // NewErrGroupNotFound returns new instance of ErrGroupNotFound | |||||
| func NewErrGroupNotFound(groupUUID string) ErrGroupNotFound { | |||||
| return ErrGroupNotFound{groupUUID: groupUUID} | |||||
| } | |||||
| // Error implements error interface | |||||
| func (e ErrGroupNotFound) Error() string { | |||||
| return fmt.Sprintf("Group not found: %v", e.groupUUID) | |||||
| } | |||||
| // ErrTasknotFound ... | |||||
| type ErrTasknotFound struct { | |||||
| taskUUID string | |||||
| } | |||||
| // NewErrTasknotFound returns new instance of ErrTasknotFound | |||||
| func NewErrTasknotFound(taskUUID string) ErrTasknotFound { | |||||
| return ErrTasknotFound{taskUUID: taskUUID} | |||||
| } | |||||
| // Error implements error interface | |||||
| func (e ErrTasknotFound) Error() string { | |||||
| return fmt.Sprintf("Task not found: %v", e.taskUUID) | |||||
| } | |||||
| // Backend represents an "eager" in-memory result backend | |||||
| type Backend struct { | |||||
| common.Backend | |||||
| groups map[string]struct{} | |||||
| } | |||||
| // New creates EagerBackend instance | |||||
| func New() iface.Backend { | |||||
| return &Backend{ | |||||
| Backend: common.NewBackend(new(config.Config)), | |||||
| groups: make(map[string]struct{}), | |||||
| } | |||||
| } | |||||
| // InitGroup creates and saves a group meta data object | |||||
| func (b *Backend) InitGroup(groupUUID string, taskUUIDs []string) error { | |||||
| b.groups[groupUUID] = struct{}{} | |||||
| return nil | |||||
| } | |||||
| // GroupCompleted returns true if all tasks in a group finished | |||||
| func (b *Backend) GroupCompleted(groupUUID string, groupTaskCount int) (bool, error) { | |||||
| _, ok := b.groups[groupUUID] | |||||
| if !ok { | |||||
| return false, NewErrGroupNotFound(groupUUID) | |||||
| } | |||||
| return true, nil | |||||
| } | |||||
| // GroupTaskStates returns states of all tasks in the group | |||||
| func (b *Backend) GroupTaskStates(groupUUID string, groupTaskCount int) ([]*tasks.TaskState, error) { | |||||
| _, ok := b.groups[groupUUID] | |||||
| if !ok { | |||||
| return nil, NewErrGroupNotFound(groupUUID) | |||||
| } | |||||
| ret := make([]*tasks.TaskState, 0, groupTaskCount) | |||||
| return ret, nil | |||||
| } | |||||
| // TriggerChord flags chord as triggered in the backend storage to make sure | |||||
| // chord is never trigerred multiple times. Returns a boolean flag to indicate | |||||
| // whether the worker should trigger chord (true) or no if it has been triggered | |||||
| // already (false) | |||||
| func (b *Backend) TriggerChord(groupUUID string) (bool, error) { | |||||
| return true, nil | |||||
| } | |||||
| // SetStatePending updates task state to PENDING | |||||
| func (b *Backend) SetStatePending(signature *tasks.Signature) error { | |||||
| state := tasks.NewPendingTaskState(signature) | |||||
| return b.updateState(state) | |||||
| } | |||||
| // SetStateReceived updates task state to RECEIVED | |||||
| func (b *Backend) SetStateReceived(signature *tasks.Signature) error { | |||||
| state := tasks.NewReceivedTaskState(signature) | |||||
| return b.updateState(state) | |||||
| } | |||||
| // SetStateStarted updates task state to STARTED | |||||
| func (b *Backend) SetStateStarted(signature *tasks.Signature) error { | |||||
| state := tasks.NewStartedTaskState(signature) | |||||
| return b.updateState(state) | |||||
| } | |||||
| // SetStateRetry updates task state to RETRY | |||||
| func (b *Backend) SetStateRetry(signature *tasks.Signature) error { | |||||
| state := tasks.NewRetryTaskState(signature) | |||||
| return b.updateState(state) | |||||
| } | |||||
| // SetStateSuccess updates task state to SUCCESS | |||||
| func (b *Backend) SetStateSuccess(signature *tasks.Signature, results []*tasks.TaskResult) error { | |||||
| state := tasks.NewSuccessTaskState(signature, results) | |||||
| return b.updateState(state) | |||||
| } | |||||
| // SetStateFailure updates task state to FAILURE | |||||
| func (b *Backend) SetStateFailure(signature *tasks.Signature, err string) error { | |||||
| state := tasks.NewFailureTaskState(signature, err) | |||||
| return b.updateState(state) | |||||
| } | |||||
| // GetState returns the latest task state | |||||
| func (b *Backend) GetState(taskUUID string) (*tasks.TaskState, error) { | |||||
| return nil, NewErrTasknotFound(taskUUID) | |||||
| } | |||||
| // PurgeState deletes stored task state | |||||
| func (b *Backend) PurgeState(taskUUID string) error { | |||||
| return NewErrTasknotFound(taskUUID) | |||||
| } | |||||
| // PurgeGroupMeta deletes stored group meta data | |||||
| func (b *Backend) PurgeGroupMeta(groupUUID string) error { | |||||
| _, ok := b.groups[groupUUID] | |||||
| if !ok { | |||||
| return NewErrGroupNotFound(groupUUID) | |||||
| } | |||||
| return nil | |||||
| } | |||||
| func (b *Backend) updateState(s *tasks.TaskState) error { | |||||
| // simulate the behavior of json marshal/unmarshal | |||||
| return nil | |||||
| } | |||||
| @@ -0,0 +1,338 @@ | |||||
| package redis | |||||
| import ( | |||||
| "bytes" | |||||
| "encoding/json" | |||||
| "fmt" | |||||
| "sync" | |||||
| "time" | |||||
| "github.com/RichardKnop/machinery/v1/backends/iface" | |||||
| "github.com/RichardKnop/machinery/v1/common" | |||||
| "github.com/RichardKnop/machinery/v1/config" | |||||
| "github.com/RichardKnop/machinery/v1/log" | |||||
| "github.com/RichardKnop/machinery/v1/tasks" | |||||
| "github.com/RichardKnop/redsync" | |||||
| "github.com/gomodule/redigo/redis" | |||||
| ) | |||||
| // Backend represents a Redis result backend | |||||
| type Backend struct { | |||||
| common.Backend | |||||
| host string | |||||
| password string | |||||
| db int | |||||
| pool *redis.Pool | |||||
| // If set, path to a socket file overrides hostname | |||||
| socketPath string | |||||
| redsync *redsync.Redsync | |||||
| redisOnce sync.Once | |||||
| common.RedisConnector | |||||
| } | |||||
| // New creates Backend instance | |||||
| func New(cnf *config.Config, host, password, socketPath string, db int) iface.Backend { | |||||
| return &Backend{ | |||||
| Backend: common.NewBackend(cnf), | |||||
| host: host, | |||||
| db: db, | |||||
| password: password, | |||||
| socketPath: socketPath, | |||||
| } | |||||
| } | |||||
| // InitGroup creates and saves a group meta data object | |||||
| func (b *Backend) InitGroup(groupUUID string, taskUUIDs []string) error { | |||||
| groupMeta := &tasks.GroupMeta{ | |||||
| GroupUUID: groupUUID, | |||||
| TaskUUIDs: taskUUIDs, | |||||
| CreatedAt: time.Now().UTC(), | |||||
| } | |||||
| encoded, err := json.Marshal(groupMeta) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| conn := b.open() | |||||
| defer conn.Close() | |||||
| _, err = conn.Do("SET", groupUUID, encoded) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| return b.setExpirationTime(groupUUID) | |||||
| } | |||||
| // GroupCompleted returns true if all tasks in a group finished | |||||
| func (b *Backend) GroupCompleted(groupUUID string, groupTaskCount int) (bool, error) { | |||||
| groupMeta, err := b.getGroupMeta(groupUUID) | |||||
| if err != nil { | |||||
| return false, err | |||||
| } | |||||
| taskStates, err := b.getStates(groupMeta.TaskUUIDs...) | |||||
| if err != nil { | |||||
| return false, err | |||||
| } | |||||
| var countSuccessTasks = 0 | |||||
| for _, taskState := range taskStates { | |||||
| if taskState.IsCompleted() { | |||||
| countSuccessTasks++ | |||||
| } | |||||
| } | |||||
| return countSuccessTasks == groupTaskCount, nil | |||||
| } | |||||
| // GroupTaskStates returns states of all tasks in the group | |||||
| func (b *Backend) GroupTaskStates(groupUUID string, groupTaskCount int) ([]*tasks.TaskState, error) { | |||||
| groupMeta, err := b.getGroupMeta(groupUUID) | |||||
| if err != nil { | |||||
| return []*tasks.TaskState{}, err | |||||
| } | |||||
| return b.getStates(groupMeta.TaskUUIDs...) | |||||
| } | |||||
| // TriggerChord flags chord as triggered in the backend storage to make sure | |||||
| // chord is never trigerred multiple times. Returns a boolean flag to indicate | |||||
| // whether the worker should trigger chord (true) or no if it has been triggered | |||||
| // already (false) | |||||
| func (b *Backend) TriggerChord(groupUUID string) (bool, error) { | |||||
| conn := b.open() | |||||
| defer conn.Close() | |||||
| m := b.redsync.NewMutex("TriggerChordMutex") | |||||
| if err := m.Lock(); err != nil { | |||||
| return false, err | |||||
| } | |||||
| defer m.Unlock() | |||||
| groupMeta, err := b.getGroupMeta(groupUUID) | |||||
| if err != nil { | |||||
| return false, err | |||||
| } | |||||
| // Chord has already been triggered, return false (should not trigger again) | |||||
| if groupMeta.ChordTriggered { | |||||
| return false, nil | |||||
| } | |||||
| // Set flag to true | |||||
| groupMeta.ChordTriggered = true | |||||
| // Update the group meta | |||||
| encoded, err := json.Marshal(&groupMeta) | |||||
| if err != nil { | |||||
| return false, err | |||||
| } | |||||
| _, err = conn.Do("SET", groupUUID, encoded) | |||||
| if err != nil { | |||||
| return false, err | |||||
| } | |||||
| return true, b.setExpirationTime(groupUUID) | |||||
| } | |||||
| func (b *Backend) mergeNewTaskState(newState *tasks.TaskState) { | |||||
| state, err := b.GetState(newState.TaskUUID) | |||||
| if err == nil { | |||||
| newState.CreatedAt = state.CreatedAt | |||||
| newState.TaskName = state.TaskName | |||||
| } | |||||
| } | |||||
| // SetStatePending updates task state to PENDING | |||||
| func (b *Backend) SetStatePending(signature *tasks.Signature) error { | |||||
| taskState := tasks.NewPendingTaskState(signature) | |||||
| return b.updateState(taskState) | |||||
| } | |||||
| // SetStateReceived updates task state to RECEIVED | |||||
| func (b *Backend) SetStateReceived(signature *tasks.Signature) error { | |||||
| taskState := tasks.NewReceivedTaskState(signature) | |||||
| b.mergeNewTaskState(taskState) | |||||
| return b.updateState(taskState) | |||||
| } | |||||
| // SetStateStarted updates task state to STARTED | |||||
| func (b *Backend) SetStateStarted(signature *tasks.Signature) error { | |||||
| taskState := tasks.NewStartedTaskState(signature) | |||||
| b.mergeNewTaskState(taskState) | |||||
| return b.updateState(taskState) | |||||
| } | |||||
| // SetStateRetry updates task state to RETRY | |||||
| func (b *Backend) SetStateRetry(signature *tasks.Signature) error { | |||||
| taskState := tasks.NewRetryTaskState(signature) | |||||
| b.mergeNewTaskState(taskState) | |||||
| return b.updateState(taskState) | |||||
| } | |||||
| // SetStateSuccess updates task state to SUCCESS | |||||
| func (b *Backend) SetStateSuccess(signature *tasks.Signature, results []*tasks.TaskResult) error { | |||||
| taskState := tasks.NewSuccessTaskState(signature, results) | |||||
| b.mergeNewTaskState(taskState) | |||||
| return b.updateState(taskState) | |||||
| } | |||||
| // SetStateFailure updates task state to FAILURE | |||||
| func (b *Backend) SetStateFailure(signature *tasks.Signature, err string) error { | |||||
| taskState := tasks.NewFailureTaskState(signature, err) | |||||
| b.mergeNewTaskState(taskState) | |||||
| return b.updateState(taskState) | |||||
| } | |||||
| // GetState returns the latest task state | |||||
| func (b *Backend) GetState(taskUUID string) (*tasks.TaskState, error) { | |||||
| conn := b.open() | |||||
| defer conn.Close() | |||||
| item, err := redis.Bytes(conn.Do("GET", taskUUID)) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| state := new(tasks.TaskState) | |||||
| decoder := json.NewDecoder(bytes.NewReader(item)) | |||||
| decoder.UseNumber() | |||||
| if err := decoder.Decode(state); err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return state, nil | |||||
| } | |||||
| // PurgeState deletes stored task state | |||||
| func (b *Backend) PurgeState(taskUUID string) error { | |||||
| conn := b.open() | |||||
| defer conn.Close() | |||||
| _, err := conn.Do("DEL", taskUUID) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| return nil | |||||
| } | |||||
| // PurgeGroupMeta deletes stored group meta data | |||||
| func (b *Backend) PurgeGroupMeta(groupUUID string) error { | |||||
| conn := b.open() | |||||
| defer conn.Close() | |||||
| _, err := conn.Do("DEL", groupUUID) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| return nil | |||||
| } | |||||
| // getGroupMeta retrieves group meta data, convenience function to avoid repetition | |||||
| func (b *Backend) getGroupMeta(groupUUID string) (*tasks.GroupMeta, error) { | |||||
| conn := b.open() | |||||
| defer conn.Close() | |||||
| item, err := redis.Bytes(conn.Do("GET", groupUUID)) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| groupMeta := new(tasks.GroupMeta) | |||||
| decoder := json.NewDecoder(bytes.NewReader(item)) | |||||
| decoder.UseNumber() | |||||
| if err := decoder.Decode(groupMeta); err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return groupMeta, nil | |||||
| } | |||||
| // getStates returns multiple task states | |||||
| func (b *Backend) getStates(taskUUIDs ...string) ([]*tasks.TaskState, error) { | |||||
| taskStates := make([]*tasks.TaskState, len(taskUUIDs)) | |||||
| conn := b.open() | |||||
| defer conn.Close() | |||||
| // conn.Do requires []interface{}... can't pass []string unfortunately | |||||
| taskUUIDInterfaces := make([]interface{}, len(taskUUIDs)) | |||||
| for i, taskUUID := range taskUUIDs { | |||||
| taskUUIDInterfaces[i] = interface{}(taskUUID) | |||||
| } | |||||
| reply, err := redis.Values(conn.Do("MGET", taskUUIDInterfaces...)) | |||||
| if err != nil { | |||||
| return taskStates, err | |||||
| } | |||||
| for i, value := range reply { | |||||
| stateBytes, ok := value.([]byte) | |||||
| if !ok { | |||||
| return taskStates, fmt.Errorf("Expected byte array, instead got: %v", value) | |||||
| } | |||||
| taskState := new(tasks.TaskState) | |||||
| decoder := json.NewDecoder(bytes.NewReader(stateBytes)) | |||||
| decoder.UseNumber() | |||||
| if err := decoder.Decode(taskState); err != nil { | |||||
| log.ERROR.Print(err) | |||||
| return taskStates, err | |||||
| } | |||||
| taskStates[i] = taskState | |||||
| } | |||||
| return taskStates, nil | |||||
| } | |||||
| // updateState saves current task state | |||||
| func (b *Backend) updateState(taskState *tasks.TaskState) error { | |||||
| conn := b.open() | |||||
| defer conn.Close() | |||||
| encoded, err := json.Marshal(taskState) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| _, err = conn.Do("SET", taskState.TaskUUID, encoded) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| return b.setExpirationTime(taskState.TaskUUID) | |||||
| } | |||||
| // setExpirationTime sets expiration timestamp on a stored task state | |||||
| func (b *Backend) setExpirationTime(key string) error { | |||||
| expiresIn := b.GetConfig().ResultsExpireIn | |||||
| if expiresIn == 0 { | |||||
| // // expire results after 1 hour by default | |||||
| expiresIn = config.DefaultResultsExpireIn | |||||
| } | |||||
| expirationTimestamp := int32(time.Now().Unix() + int64(expiresIn)) | |||||
| conn := b.open() | |||||
| defer conn.Close() | |||||
| _, err := conn.Do("EXPIREAT", key, expirationTimestamp) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| return nil | |||||
| } | |||||
| // open returns or creates instance of Redis connection | |||||
| func (b *Backend) open() redis.Conn { | |||||
| b.redisOnce.Do(func() { | |||||
| b.pool = b.NewPool(b.socketPath, b.host, b.password, b.db, b.GetConfig().Redis, b.GetConfig().TLSConfig) | |||||
| b.redsync = redsync.New([]redsync.Pool{b.pool}) | |||||
| }) | |||||
| return b.pool.Get() | |||||
| } | |||||
| @@ -0,0 +1,256 @@ | |||||
| package result | |||||
| import ( | |||||
| "errors" | |||||
| "reflect" | |||||
| "time" | |||||
| "github.com/RichardKnop/machinery/v1/backends/iface" | |||||
| "github.com/RichardKnop/machinery/v1/tasks" | |||||
| ) | |||||
| var ( | |||||
| // ErrBackendNotConfigured ... | |||||
| ErrBackendNotConfigured = errors.New("Result backend not configured") | |||||
| // ErrTimeoutReached ... | |||||
| ErrTimeoutReached = errors.New("Timeout reached") | |||||
| ) | |||||
| // AsyncResult represents a task result | |||||
| type AsyncResult struct { | |||||
| Signature *tasks.Signature | |||||
| taskState *tasks.TaskState | |||||
| backend iface.Backend | |||||
| } | |||||
| // ChordAsyncResult represents a result of a chord | |||||
| type ChordAsyncResult struct { | |||||
| groupAsyncResults []*AsyncResult | |||||
| chordAsyncResult *AsyncResult | |||||
| backend iface.Backend | |||||
| } | |||||
| // ChainAsyncResult represents a result of a chain of tasks | |||||
| type ChainAsyncResult struct { | |||||
| asyncResults []*AsyncResult | |||||
| backend iface.Backend | |||||
| } | |||||
| // NewAsyncResult creates AsyncResult instance | |||||
| func NewAsyncResult(signature *tasks.Signature, backend iface.Backend) *AsyncResult { | |||||
| return &AsyncResult{ | |||||
| Signature: signature, | |||||
| taskState: new(tasks.TaskState), | |||||
| backend: backend, | |||||
| } | |||||
| } | |||||
| // NewChordAsyncResult creates ChordAsyncResult instance | |||||
| func NewChordAsyncResult(groupTasks []*tasks.Signature, chordCallback *tasks.Signature, backend iface.Backend) *ChordAsyncResult { | |||||
| asyncResults := make([]*AsyncResult, len(groupTasks)) | |||||
| for i, task := range groupTasks { | |||||
| asyncResults[i] = NewAsyncResult(task, backend) | |||||
| } | |||||
| return &ChordAsyncResult{ | |||||
| groupAsyncResults: asyncResults, | |||||
| chordAsyncResult: NewAsyncResult(chordCallback, backend), | |||||
| backend: backend, | |||||
| } | |||||
| } | |||||
| // NewChainAsyncResult creates ChainAsyncResult instance | |||||
| func NewChainAsyncResult(tasks []*tasks.Signature, backend iface.Backend) *ChainAsyncResult { | |||||
| asyncResults := make([]*AsyncResult, len(tasks)) | |||||
| for i, task := range tasks { | |||||
| asyncResults[i] = NewAsyncResult(task, backend) | |||||
| } | |||||
| return &ChainAsyncResult{ | |||||
| asyncResults: asyncResults, | |||||
| backend: backend, | |||||
| } | |||||
| } | |||||
| // Touch the state and don't wait | |||||
| func (asyncResult *AsyncResult) Touch() ([]reflect.Value, error) { | |||||
| if asyncResult.backend == nil { | |||||
| return nil, ErrBackendNotConfigured | |||||
| } | |||||
| asyncResult.GetState() | |||||
| // Purge state if we are using AMQP backend | |||||
| if asyncResult.backend.IsAMQP() && asyncResult.taskState.IsCompleted() { | |||||
| asyncResult.backend.PurgeState(asyncResult.taskState.TaskUUID) | |||||
| } | |||||
| if asyncResult.taskState.IsFailure() { | |||||
| return nil, errors.New(asyncResult.taskState.Error) | |||||
| } | |||||
| if asyncResult.taskState.IsSuccess() { | |||||
| return tasks.ReflectTaskResults(asyncResult.taskState.Results) | |||||
| } | |||||
| return nil, nil | |||||
| } | |||||
| // Get returns task results (synchronous blocking call) | |||||
| func (asyncResult *AsyncResult) Get(sleepDuration time.Duration) ([]reflect.Value, error) { | |||||
| for { | |||||
| results, err := asyncResult.Touch() | |||||
| if results == nil && err == nil { | |||||
| time.Sleep(sleepDuration) | |||||
| } else { | |||||
| return results, err | |||||
| } | |||||
| } | |||||
| } | |||||
| // GetWithTimeout returns task results with a timeout (synchronous blocking call) | |||||
| func (asyncResult *AsyncResult) GetWithTimeout(timeoutDuration, sleepDuration time.Duration) ([]reflect.Value, error) { | |||||
| timeout := time.NewTimer(timeoutDuration) | |||||
| for { | |||||
| select { | |||||
| case <-timeout.C: | |||||
| return nil, ErrTimeoutReached | |||||
| default: | |||||
| results, err := asyncResult.Touch() | |||||
| if results == nil && err == nil { | |||||
| time.Sleep(sleepDuration) | |||||
| } else { | |||||
| return results, err | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| // GetState returns latest task state | |||||
| func (asyncResult *AsyncResult) GetState() *tasks.TaskState { | |||||
| if asyncResult.taskState.IsCompleted() { | |||||
| return asyncResult.taskState | |||||
| } | |||||
| taskState, err := asyncResult.backend.GetState(asyncResult.Signature.UUID) | |||||
| if err == nil { | |||||
| asyncResult.taskState = taskState | |||||
| } | |||||
| return asyncResult.taskState | |||||
| } | |||||
| // Get returns results of a chain of tasks (synchronous blocking call) | |||||
| func (chainAsyncResult *ChainAsyncResult) Get(sleepDuration time.Duration) ([]reflect.Value, error) { | |||||
| if chainAsyncResult.backend == nil { | |||||
| return nil, ErrBackendNotConfigured | |||||
| } | |||||
| var ( | |||||
| results []reflect.Value | |||||
| err error | |||||
| ) | |||||
| for _, asyncResult := range chainAsyncResult.asyncResults { | |||||
| results, err = asyncResult.Get(sleepDuration) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| } | |||||
| return results, err | |||||
| } | |||||
| // Get returns result of a chord (synchronous blocking call) | |||||
| func (chordAsyncResult *ChordAsyncResult) Get(sleepDuration time.Duration) ([]reflect.Value, error) { | |||||
| if chordAsyncResult.backend == nil { | |||||
| return nil, ErrBackendNotConfigured | |||||
| } | |||||
| var err error | |||||
| for _, asyncResult := range chordAsyncResult.groupAsyncResults { | |||||
| _, err = asyncResult.Get(sleepDuration) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| } | |||||
| return chordAsyncResult.chordAsyncResult.Get(sleepDuration) | |||||
| } | |||||
| // GetWithTimeout returns results of a chain of tasks with timeout (synchronous blocking call) | |||||
| func (chainAsyncResult *ChainAsyncResult) GetWithTimeout(timeoutDuration, sleepDuration time.Duration) ([]reflect.Value, error) { | |||||
| if chainAsyncResult.backend == nil { | |||||
| return nil, ErrBackendNotConfigured | |||||
| } | |||||
| var ( | |||||
| results []reflect.Value | |||||
| err error | |||||
| ) | |||||
| timeout := time.NewTimer(timeoutDuration) | |||||
| ln := len(chainAsyncResult.asyncResults) | |||||
| lastResult := chainAsyncResult.asyncResults[ln-1] | |||||
| for { | |||||
| select { | |||||
| case <-timeout.C: | |||||
| return nil, ErrTimeoutReached | |||||
| default: | |||||
| for _, asyncResult := range chainAsyncResult.asyncResults { | |||||
| _, errcur := asyncResult.Touch() | |||||
| if errcur != nil { | |||||
| return nil, err | |||||
| } | |||||
| } | |||||
| results, err = lastResult.Touch() | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| if results != nil { | |||||
| return results, err | |||||
| } | |||||
| time.Sleep(sleepDuration) | |||||
| } | |||||
| } | |||||
| } | |||||
| // GetWithTimeout returns result of a chord with a timeout (synchronous blocking call) | |||||
| func (chordAsyncResult *ChordAsyncResult) GetWithTimeout(timeoutDuration, sleepDuration time.Duration) ([]reflect.Value, error) { | |||||
| if chordAsyncResult.backend == nil { | |||||
| return nil, ErrBackendNotConfigured | |||||
| } | |||||
| var ( | |||||
| results []reflect.Value | |||||
| err error | |||||
| ) | |||||
| timeout := time.NewTimer(timeoutDuration) | |||||
| for { | |||||
| select { | |||||
| case <-timeout.C: | |||||
| return nil, ErrTimeoutReached | |||||
| default: | |||||
| for _, asyncResult := range chordAsyncResult.groupAsyncResults { | |||||
| _, errcur := asyncResult.Touch() | |||||
| if errcur != nil { | |||||
| return nil, err | |||||
| } | |||||
| } | |||||
| results, err = chordAsyncResult.chordAsyncResult.Touch() | |||||
| if err != nil { | |||||
| return nil, nil | |||||
| } | |||||
| if results != nil { | |||||
| return results, err | |||||
| } | |||||
| time.Sleep(sleepDuration) | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,424 @@ | |||||
| package amqp | |||||
| import ( | |||||
| "bytes" | |||||
| "context" | |||||
| "encoding/json" | |||||
| "fmt" | |||||
| "sync" | |||||
| "time" | |||||
| "github.com/RichardKnop/machinery/v1/brokers/errs" | |||||
| "github.com/RichardKnop/machinery/v1/brokers/iface" | |||||
| "github.com/RichardKnop/machinery/v1/common" | |||||
| "github.com/RichardKnop/machinery/v1/config" | |||||
| "github.com/RichardKnop/machinery/v1/log" | |||||
| "github.com/RichardKnop/machinery/v1/tasks" | |||||
| "github.com/pkg/errors" | |||||
| "github.com/streadway/amqp" | |||||
| ) | |||||
| type AMQPConnection struct { | |||||
| queueName string | |||||
| connection *amqp.Connection | |||||
| channel *amqp.Channel | |||||
| queue amqp.Queue | |||||
| confirmation <-chan amqp.Confirmation | |||||
| errorchan <-chan *amqp.Error | |||||
| cleanup chan struct{} | |||||
| } | |||||
| // Broker represents an AMQP broker | |||||
| type Broker struct { | |||||
| common.Broker | |||||
| common.AMQPConnector | |||||
| processingWG sync.WaitGroup // use wait group to make sure task processing completes on interrupt signal | |||||
| connections map[string]*AMQPConnection | |||||
| connectionsMutex sync.RWMutex | |||||
| } | |||||
| // New creates new Broker instance | |||||
| func New(cnf *config.Config) iface.Broker { | |||||
| return &Broker{Broker: common.NewBroker(cnf), AMQPConnector: common.AMQPConnector{}, connections: make(map[string]*AMQPConnection)} | |||||
| } | |||||
| // StartConsuming enters a loop and waits for incoming messages | |||||
| func (b *Broker) StartConsuming(consumerTag string, concurrency int, taskProcessor iface.TaskProcessor) (bool, error) { | |||||
| b.Broker.StartConsuming(consumerTag, concurrency, taskProcessor) | |||||
| queueName := taskProcessor.CustomQueue() | |||||
| if queueName == "" { | |||||
| queueName = b.GetConfig().DefaultQueue | |||||
| } | |||||
| conn, channel, queue, _, amqpCloseChan, err := b.Connect( | |||||
| b.GetConfig().Broker, | |||||
| b.GetConfig().TLSConfig, | |||||
| b.GetConfig().AMQP.Exchange, // exchange name | |||||
| b.GetConfig().AMQP.ExchangeType, // exchange type | |||||
| queueName, // queue name | |||||
| true, // queue durable | |||||
| false, // queue delete when unused | |||||
| b.GetConfig().AMQP.BindingKey, // queue binding key | |||||
| nil, // exchange declare args | |||||
| nil, // queue declare args | |||||
| amqp.Table(b.GetConfig().AMQP.QueueBindingArgs), // queue binding args | |||||
| ) | |||||
| if err != nil { | |||||
| b.GetRetryFunc()(b.GetRetryStopChan()) | |||||
| return b.GetRetry(), err | |||||
| } | |||||
| defer b.Close(channel, conn) | |||||
| if err = channel.Qos( | |||||
| b.GetConfig().AMQP.PrefetchCount, | |||||
| 0, // prefetch size | |||||
| false, // global | |||||
| ); err != nil { | |||||
| return b.GetRetry(), fmt.Errorf("Channel qos error: %s", err) | |||||
| } | |||||
| deliveries, err := channel.Consume( | |||||
| queue.Name, // queue | |||||
| consumerTag, // consumer tag | |||||
| false, // auto-ack | |||||
| false, // exclusive | |||||
| false, // no-local | |||||
| false, // no-wait | |||||
| nil, // arguments | |||||
| ) | |||||
| if err != nil { | |||||
| return b.GetRetry(), fmt.Errorf("Queue consume error: %s", err) | |||||
| } | |||||
| log.INFO.Print("[*] Waiting for messages. To exit press CTRL+C") | |||||
| if err := b.consume(deliveries, concurrency, taskProcessor, amqpCloseChan); err != nil { | |||||
| return b.GetRetry(), err | |||||
| } | |||||
| // Waiting for any tasks being processed to finish | |||||
| b.processingWG.Wait() | |||||
| return b.GetRetry(), nil | |||||
| } | |||||
| // StopConsuming quits the loop | |||||
| func (b *Broker) StopConsuming() { | |||||
| b.Broker.StopConsuming() | |||||
| // Waiting for any tasks being processed to finish | |||||
| b.processingWG.Wait() | |||||
| } | |||||
| // GetOrOpenConnection will return a connection on a particular queue name. Open connections | |||||
| // are saved to avoid having to reopen connection for multiple queues | |||||
| func (b *Broker) GetOrOpenConnection(queueName string, queueBindingKey string, exchangeDeclareArgs, queueDeclareArgs, queueBindingArgs amqp.Table) (*AMQPConnection, error) { | |||||
| var err error | |||||
| b.connectionsMutex.Lock() | |||||
| defer b.connectionsMutex.Unlock() | |||||
| conn, ok := b.connections[queueName] | |||||
| if !ok { | |||||
| conn = &AMQPConnection{ | |||||
| queueName: queueName, | |||||
| cleanup: make(chan struct{}), | |||||
| } | |||||
| conn.connection, conn.channel, conn.queue, conn.confirmation, conn.errorchan, err = b.Connect( | |||||
| b.GetConfig().Broker, | |||||
| b.GetConfig().TLSConfig, | |||||
| b.GetConfig().AMQP.Exchange, // exchange name | |||||
| b.GetConfig().AMQP.ExchangeType, // exchange type | |||||
| queueName, // queue name | |||||
| true, // queue durable | |||||
| false, // queue delete when unused | |||||
| queueBindingKey, // queue binding key | |||||
| exchangeDeclareArgs, // exchange declare args | |||||
| queueDeclareArgs, // queue declare args | |||||
| queueBindingArgs, // queue binding args | |||||
| ) | |||||
| if err != nil { | |||||
| return nil, errors.Wrapf(err, "Failed to connect to queue %s", queueName) | |||||
| } | |||||
| // Reconnect to the channel if it disconnects/errors out | |||||
| go func() { | |||||
| select { | |||||
| case err = <-conn.errorchan: | |||||
| log.INFO.Printf("Error occured on queue: %s. Reconnecting", queueName) | |||||
| b.connectionsMutex.Lock() | |||||
| delete(b.connections, queueName) | |||||
| b.connectionsMutex.Unlock() | |||||
| _, err := b.GetOrOpenConnection(queueName, queueBindingKey, exchangeDeclareArgs, queueDeclareArgs, queueBindingArgs) | |||||
| if err != nil { | |||||
| log.ERROR.Printf("Failed to reopen queue: %s.", queueName) | |||||
| } | |||||
| case <-conn.cleanup: | |||||
| return | |||||
| } | |||||
| return | |||||
| }() | |||||
| b.connections[queueName] = conn | |||||
| } | |||||
| return conn, nil | |||||
| } | |||||
| func (b *Broker) CloseConnections() error { | |||||
| b.connectionsMutex.Lock() | |||||
| defer b.connectionsMutex.Unlock() | |||||
| for key, conn := range b.connections { | |||||
| if err := b.Close(conn.channel, conn.connection); err != nil { | |||||
| log.ERROR.Print("Failed to close channel") | |||||
| return nil | |||||
| } | |||||
| close(conn.cleanup) | |||||
| delete(b.connections, key) | |||||
| } | |||||
| return nil | |||||
| } | |||||
| // Publish places a new message on the default queue | |||||
| func (b *Broker) Publish(ctx context.Context, signature *tasks.Signature) error { | |||||
| // Adjust routing key (this decides which queue the message will be published to) | |||||
| b.AdjustRoutingKey(signature) | |||||
| msg, err := json.Marshal(signature) | |||||
| if err != nil { | |||||
| return fmt.Errorf("JSON marshal error: %s", err) | |||||
| } | |||||
| // Check the ETA signature field, if it is set and it is in the future, | |||||
| // delay the task | |||||
| if signature.ETA != nil { | |||||
| now := time.Now().UTC() | |||||
| if signature.ETA.After(now) { | |||||
| delayMs := int64(signature.ETA.Sub(now) / time.Millisecond) | |||||
| return b.delay(signature, delayMs) | |||||
| } | |||||
| } | |||||
| queue := b.GetConfig().DefaultQueue | |||||
| bindingKey := b.GetConfig().AMQP.BindingKey // queue binding key | |||||
| if b.isDirectExchange() { | |||||
| queue = signature.RoutingKey | |||||
| bindingKey = signature.RoutingKey | |||||
| } | |||||
| connection, err := b.GetOrOpenConnection( | |||||
| queue, | |||||
| bindingKey, // queue binding key | |||||
| nil, // exchange declare args | |||||
| nil, // queue declare args | |||||
| amqp.Table(b.GetConfig().AMQP.QueueBindingArgs), // queue binding args | |||||
| ) | |||||
| if err != nil { | |||||
| return errors.Wrapf(err, "Failed to get a connection for queue %s", queue) | |||||
| } | |||||
| channel := connection.channel | |||||
| confirmsChan := connection.confirmation | |||||
| if err := channel.Publish( | |||||
| b.GetConfig().AMQP.Exchange, // exchange name | |||||
| signature.RoutingKey, // routing key | |||||
| false, // mandatory | |||||
| false, // immediate | |||||
| amqp.Publishing{ | |||||
| Headers: amqp.Table(signature.Headers), | |||||
| ContentType: "application/json", | |||||
| Body: msg, | |||||
| DeliveryMode: amqp.Persistent, | |||||
| }, | |||||
| ); err != nil { | |||||
| return errors.Wrap(err, "Failed to publish task") | |||||
| } | |||||
| confirmed := <-confirmsChan | |||||
| if confirmed.Ack { | |||||
| return nil | |||||
| } | |||||
| return fmt.Errorf("Failed delivery of delivery tag: %v", confirmed.DeliveryTag) | |||||
| } | |||||
| // consume takes delivered messages from the channel and manages a worker pool | |||||
| // to process tasks concurrently | |||||
| func (b *Broker) consume(deliveries <-chan amqp.Delivery, concurrency int, taskProcessor iface.TaskProcessor, amqpCloseChan <-chan *amqp.Error) error { | |||||
| pool := make(chan struct{}, concurrency) | |||||
| // initialize worker pool with maxWorkers workers | |||||
| go func() { | |||||
| for i := 0; i < concurrency; i++ { | |||||
| pool <- struct{}{} | |||||
| } | |||||
| }() | |||||
| errorsChan := make(chan error) | |||||
| for { | |||||
| select { | |||||
| case amqpErr := <-amqpCloseChan: | |||||
| return amqpErr | |||||
| case err := <-errorsChan: | |||||
| return err | |||||
| case d := <-deliveries: | |||||
| if concurrency > 0 { | |||||
| // get worker from pool (blocks until one is available) | |||||
| <-pool | |||||
| } | |||||
| b.processingWG.Add(1) | |||||
| // Consume the task inside a gotourine so multiple tasks | |||||
| // can be processed concurrently | |||||
| go func() { | |||||
| if err := b.consumeOne(d, taskProcessor); err != nil { | |||||
| errorsChan <- err | |||||
| } | |||||
| b.processingWG.Done() | |||||
| if concurrency > 0 { | |||||
| // give worker back to pool | |||||
| pool <- struct{}{} | |||||
| } | |||||
| }() | |||||
| case <-b.GetStopChan(): | |||||
| return nil | |||||
| } | |||||
| } | |||||
| } | |||||
| // consumeOne processes a single message using TaskProcessor | |||||
| func (b *Broker) consumeOne(delivery amqp.Delivery, taskProcessor iface.TaskProcessor) error { | |||||
| if len(delivery.Body) == 0 { | |||||
| delivery.Nack(true, false) // multiple, requeue | |||||
| return errors.New("Received an empty message") // RabbitMQ down? | |||||
| } | |||||
| var multiple, requeue = false, false | |||||
| // Unmarshal message body into signature struct | |||||
| signature := new(tasks.Signature) | |||||
| decoder := json.NewDecoder(bytes.NewReader(delivery.Body)) | |||||
| decoder.UseNumber() | |||||
| if err := decoder.Decode(signature); err != nil { | |||||
| delivery.Nack(multiple, requeue) | |||||
| return errs.NewErrCouldNotUnmarshaTaskSignature(delivery.Body, err) | |||||
| } | |||||
| // If the task is not registered, we nack it and requeue, | |||||
| // there might be different workers for processing specific tasks | |||||
| if !b.IsTaskRegistered(signature.Name) { | |||||
| if !delivery.Redelivered { | |||||
| requeue = true | |||||
| log.INFO.Printf("Task not registered with this worker. Requeing message: %s", delivery.Body) | |||||
| } | |||||
| delivery.Nack(multiple, requeue) | |||||
| return nil | |||||
| } | |||||
| log.INFO.Printf("Received new message: %s", delivery.Body) | |||||
| err := taskProcessor.Process(signature) | |||||
| delivery.Ack(multiple) | |||||
| return err | |||||
| } | |||||
| // delay a task by delayDuration miliseconds, the way it works is a new queue | |||||
| // is created without any consumers, the message is then published to this queue | |||||
| // with appropriate ttl expiration headers, after the expiration, it is sent to | |||||
| // the proper queue with consumers | |||||
| func (b *Broker) delay(signature *tasks.Signature, delayMs int64) error { | |||||
| if delayMs <= 0 { | |||||
| return errors.New("Cannot delay task by 0ms") | |||||
| } | |||||
| message, err := json.Marshal(signature) | |||||
| if err != nil { | |||||
| return fmt.Errorf("JSON marshal error: %s", err) | |||||
| } | |||||
| // It's necessary to redeclare the queue each time (to zero its TTL timer). | |||||
| queueName := fmt.Sprintf( | |||||
| "delay.%d.%s.%s", | |||||
| delayMs, // delay duration in mileseconds | |||||
| b.GetConfig().AMQP.Exchange, | |||||
| signature.RoutingKey, // routing key | |||||
| ) | |||||
| declareQueueArgs := amqp.Table{ | |||||
| // Exchange where to send messages after TTL expiration. | |||||
| "x-dead-letter-exchange": b.GetConfig().AMQP.Exchange, | |||||
| // Routing key which use when resending expired messages. | |||||
| "x-dead-letter-routing-key": signature.RoutingKey, | |||||
| // Time in milliseconds | |||||
| // after that message will expire and be sent to destination. | |||||
| "x-message-ttl": delayMs, | |||||
| // Time after that the queue will be deleted. | |||||
| "x-expires": delayMs * 2, | |||||
| } | |||||
| conn, channel, _, _, _, err := b.Connect( | |||||
| b.GetConfig().Broker, | |||||
| b.GetConfig().TLSConfig, | |||||
| b.GetConfig().AMQP.Exchange, // exchange name | |||||
| b.GetConfig().AMQP.ExchangeType, // exchange type | |||||
| queueName, // queue name | |||||
| true, // queue durable | |||||
| b.GetConfig().AMQP.AutoDelete, // queue delete when unused | |||||
| queueName, // queue binding key | |||||
| nil, // exchange declare args | |||||
| declareQueueArgs, // queue declare args | |||||
| amqp.Table(b.GetConfig().AMQP.QueueBindingArgs), // queue binding args | |||||
| ) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| defer b.Close(channel, conn) | |||||
| if err := channel.Publish( | |||||
| b.GetConfig().AMQP.Exchange, // exchange | |||||
| queueName, // routing key | |||||
| false, // mandatory | |||||
| false, // immediate | |||||
| amqp.Publishing{ | |||||
| Headers: amqp.Table(signature.Headers), | |||||
| ContentType: "application/json", | |||||
| Body: message, | |||||
| DeliveryMode: amqp.Persistent, | |||||
| }, | |||||
| ); err != nil { | |||||
| return err | |||||
| } | |||||
| return nil | |||||
| } | |||||
| func (b *Broker) isDirectExchange() bool { | |||||
| return b.GetConfig().AMQP != nil && b.GetConfig().AMQP.ExchangeType == "direct" | |||||
| } | |||||
| // AdjustRoutingKey makes sure the routing key is correct. | |||||
| // If the routing key is an empty string: | |||||
| // a) set it to binding key for direct exchange type | |||||
| // b) set it to default queue name | |||||
| func (b *Broker) AdjustRoutingKey(s *tasks.Signature) { | |||||
| if s.RoutingKey != "" { | |||||
| return | |||||
| } | |||||
| if b.isDirectExchange() { | |||||
| // The routing algorithm behind a direct exchange is simple - a message goes | |||||
| // to the queues whose binding key exactly matches the routing key of the message. | |||||
| s.RoutingKey = b.GetConfig().AMQP.BindingKey | |||||
| return | |||||
| } | |||||
| s.RoutingKey = b.GetConfig().DefaultQueue | |||||
| } | |||||
| @@ -0,0 +1,73 @@ | |||||
| package eager | |||||
| import ( | |||||
| "bytes" | |||||
| "context" | |||||
| "encoding/json" | |||||
| "errors" | |||||
| "fmt" | |||||
| "github.com/RichardKnop/machinery/v1/brokers/iface" | |||||
| "github.com/RichardKnop/machinery/v1/common" | |||||
| "github.com/RichardKnop/machinery/v1/tasks" | |||||
| ) | |||||
| // Broker represents an "eager" in-memory broker | |||||
| type Broker struct { | |||||
| worker iface.TaskProcessor | |||||
| common.Broker | |||||
| } | |||||
| // New creates new Broker instance | |||||
| func New() iface.Broker { | |||||
| return new(Broker) | |||||
| } | |||||
| // Mode interface with methods specific for this broker | |||||
| type Mode interface { | |||||
| AssignWorker(p iface.TaskProcessor) | |||||
| } | |||||
| // StartConsuming enters a loop and waits for incoming messages | |||||
| func (eagerBroker *Broker) StartConsuming(consumerTag string, concurrency int, p iface.TaskProcessor) (bool, error) { | |||||
| return true, nil | |||||
| } | |||||
| // StopConsuming quits the loop | |||||
| func (eagerBroker *Broker) StopConsuming() { | |||||
| // do nothing | |||||
| } | |||||
| // Publish places a new message on the default queue | |||||
| func (eagerBroker *Broker) Publish(ctx context.Context, task *tasks.Signature) error { | |||||
| if eagerBroker.worker == nil { | |||||
| return errors.New("worker is not assigned in eager-mode") | |||||
| } | |||||
| // faking the behavior to marshal input into json | |||||
| // and unmarshal it back | |||||
| message, err := json.Marshal(task) | |||||
| if err != nil { | |||||
| return fmt.Errorf("JSON marshal error: %s", err) | |||||
| } | |||||
| signature := new(tasks.Signature) | |||||
| decoder := json.NewDecoder(bytes.NewReader(message)) | |||||
| decoder.UseNumber() | |||||
| if err := decoder.Decode(signature); err != nil { | |||||
| return fmt.Errorf("JSON unmarshal error: %s", err) | |||||
| } | |||||
| // blocking call to the task directly | |||||
| return eagerBroker.worker.Process(signature) | |||||
| } | |||||
| // GetPendingTasks returns a slice of task.Signatures waiting in the queue | |||||
| func (eagerBroker *Broker) GetPendingTasks(queue string) ([]*tasks.Signature, error) { | |||||
| return []*tasks.Signature{}, errors.New("Not implemented") | |||||
| } | |||||
| // AssignWorker assigns a worker to the eager broker | |||||
| func (eagerBroker *Broker) AssignWorker(w iface.TaskProcessor) { | |||||
| eagerBroker.worker = w | |||||
| } | |||||
| @@ -0,0 +1,25 @@ | |||||
| package errs | |||||
| import ( | |||||
| "errors" | |||||
| "fmt" | |||||
| ) | |||||
| // ErrCouldNotUnmarshaTaskSignature ... | |||||
| type ErrCouldNotUnmarshaTaskSignature struct { | |||||
| msg []byte | |||||
| reason string | |||||
| } | |||||
| // Error implements the error interface | |||||
| func (e ErrCouldNotUnmarshaTaskSignature) Error() string { | |||||
| return fmt.Sprintf("Could not unmarshal '%s' into a task signature: %v", e.msg, e.reason) | |||||
| } | |||||
| // NewErrCouldNotUnmarshaTaskSignature returns new ErrCouldNotUnmarshaTaskSignature instance | |||||
| func NewErrCouldNotUnmarshaTaskSignature(msg []byte, err error) ErrCouldNotUnmarshaTaskSignature { | |||||
| return ErrCouldNotUnmarshaTaskSignature{msg: msg, reason: err.Error()} | |||||
| } | |||||
| // ErrConsumerStopped indicates that the operation is now illegal because of the consumer being stopped. | |||||
| var ErrConsumerStopped = errors.New("the server has been stopped") | |||||
| @@ -0,0 +1,196 @@ | |||||
| package gcppubsub | |||||
| import ( | |||||
| "bytes" | |||||
| "context" | |||||
| "encoding/json" | |||||
| "fmt" | |||||
| "time" | |||||
| "cloud.google.com/go/pubsub" | |||||
| "github.com/RichardKnop/machinery/v1/brokers/iface" | |||||
| "github.com/RichardKnop/machinery/v1/common" | |||||
| "github.com/RichardKnop/machinery/v1/config" | |||||
| "github.com/RichardKnop/machinery/v1/log" | |||||
| "github.com/RichardKnop/machinery/v1/tasks" | |||||
| ) | |||||
| // Broker represents an Google Cloud Pub/Sub broker | |||||
| type Broker struct { | |||||
| common.Broker | |||||
| service *pubsub.Client | |||||
| subscriptionName string | |||||
| MaxExtension time.Duration | |||||
| stopDone chan struct{} | |||||
| } | |||||
| // New creates new Broker instance | |||||
| func New(cnf *config.Config, projectID, subscriptionName string) (iface.Broker, error) { | |||||
| b := &Broker{Broker: common.NewBroker(cnf), stopDone: make(chan struct{})} | |||||
| b.subscriptionName = subscriptionName | |||||
| ctx := context.Background() | |||||
| if cnf.GCPPubSub != nil { | |||||
| b.MaxExtension = cnf.GCPPubSub.MaxExtension | |||||
| } | |||||
| if cnf.GCPPubSub != nil && cnf.GCPPubSub.Client != nil { | |||||
| b.service = cnf.GCPPubSub.Client | |||||
| } else { | |||||
| pubsubClient, err := pubsub.NewClient(ctx, projectID) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| b.service = pubsubClient | |||||
| cnf.GCPPubSub = &config.GCPPubSubConfig{ | |||||
| Client: pubsubClient, | |||||
| } | |||||
| } | |||||
| // Validate topic exists | |||||
| defaultQueue := b.GetConfig().DefaultQueue | |||||
| topic := b.service.Topic(defaultQueue) | |||||
| defer topic.Stop() | |||||
| topicExists, err := topic.Exists(ctx) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| if !topicExists { | |||||
| return nil, fmt.Errorf("topic does not exist, instead got %s", defaultQueue) | |||||
| } | |||||
| // Validate subscription exists | |||||
| sub := b.service.Subscription(b.subscriptionName) | |||||
| if b.MaxExtension != 0 { | |||||
| sub.ReceiveSettings.MaxExtension = b.MaxExtension | |||||
| } | |||||
| subscriptionExists, err := sub.Exists(ctx) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| if !subscriptionExists { | |||||
| return nil, fmt.Errorf("subscription does not exist, instead got %s", b.subscriptionName) | |||||
| } | |||||
| return b, nil | |||||
| } | |||||
| // StartConsuming enters a loop and waits for incoming messages | |||||
| func (b *Broker) StartConsuming(consumerTag string, concurrency int, taskProcessor iface.TaskProcessor) (bool, error) { | |||||
| b.Broker.StartConsuming(consumerTag, concurrency, taskProcessor) | |||||
| sub := b.service.Subscription(b.subscriptionName) | |||||
| if b.MaxExtension != 0 { | |||||
| sub.ReceiveSettings.MaxExtension = b.MaxExtension | |||||
| } | |||||
| sub.ReceiveSettings.NumGoroutines = concurrency | |||||
| log.INFO.Print("[*] Waiting for messages. To exit press CTRL+C") | |||||
| ctx, cancel := context.WithCancel(context.Background()) | |||||
| go func() { | |||||
| <-b.GetStopChan() | |||||
| cancel() | |||||
| }() | |||||
| for { | |||||
| err := sub.Receive(ctx, func(_ctx context.Context, msg *pubsub.Message) { | |||||
| b.consumeOne(msg, taskProcessor) | |||||
| }) | |||||
| if err == nil { | |||||
| break | |||||
| } | |||||
| log.ERROR.Printf("Error when receiving messages. Error: %v", err) | |||||
| continue | |||||
| } | |||||
| close(b.stopDone) | |||||
| return b.GetRetry(), nil | |||||
| } | |||||
| // StopConsuming quits the loop | |||||
| func (b *Broker) StopConsuming() { | |||||
| b.Broker.StopConsuming() | |||||
| // Waiting for any tasks being processed to finish | |||||
| <-b.stopDone | |||||
| } | |||||
| // Publish places a new message on the default queue or the queue pointed to | |||||
| // by the routing key | |||||
| func (b *Broker) Publish(ctx context.Context, signature *tasks.Signature) error { | |||||
| // Adjust routing key (this decides which queue the message will be published to) | |||||
| b.AdjustRoutingKey(signature) | |||||
| msg, err := json.Marshal(signature) | |||||
| if err != nil { | |||||
| return fmt.Errorf("JSON marshal error: %s", err) | |||||
| } | |||||
| topic := b.service.Topic(signature.RoutingKey) | |||||
| defer topic.Stop() | |||||
| // Check the ETA signature field, if it is set and it is in the future, | |||||
| // delay the task | |||||
| if signature.ETA != nil { | |||||
| now := time.Now().UTC() | |||||
| if signature.ETA.After(now) { | |||||
| topic.PublishSettings.DelayThreshold = signature.ETA.Sub(now) | |||||
| } | |||||
| } | |||||
| result := topic.Publish(ctx, &pubsub.Message{ | |||||
| Data: msg, | |||||
| }) | |||||
| id, err := result.Get(ctx) | |||||
| if err != nil { | |||||
| log.ERROR.Printf("Error when sending a message: %v", err) | |||||
| return err | |||||
| } | |||||
| log.INFO.Printf("Sending a message successfully, server-generated message ID %v", id) | |||||
| return nil | |||||
| } | |||||
| // consumeOne processes a single message using TaskProcessor | |||||
| func (b *Broker) consumeOne(delivery *pubsub.Message, taskProcessor iface.TaskProcessor) { | |||||
| if len(delivery.Data) == 0 { | |||||
| delivery.Nack() | |||||
| log.ERROR.Printf("received an empty message, the delivery was %v", delivery) | |||||
| } | |||||
| sig := new(tasks.Signature) | |||||
| decoder := json.NewDecoder(bytes.NewBuffer(delivery.Data)) | |||||
| decoder.UseNumber() | |||||
| if err := decoder.Decode(sig); err != nil { | |||||
| delivery.Nack() | |||||
| log.ERROR.Printf("unmarshal error. the delivery is %v", delivery) | |||||
| } | |||||
| // If the task is not registered return an error | |||||
| // and leave the message in the queue | |||||
| if !b.IsTaskRegistered(sig.Name) { | |||||
| delivery.Nack() | |||||
| log.ERROR.Printf("task %s is not registered", sig.Name) | |||||
| } | |||||
| err := taskProcessor.Process(sig) | |||||
| if err != nil { | |||||
| delivery.Nack() | |||||
| log.ERROR.Printf("Failed process of task", err) | |||||
| } | |||||
| // Call Ack() after successfully consuming and processing the message | |||||
| delivery.Ack() | |||||
| } | |||||
| @@ -0,0 +1,27 @@ | |||||
| package iface | |||||
| import ( | |||||
| "context" | |||||
| "github.com/RichardKnop/machinery/v1/config" | |||||
| "github.com/RichardKnop/machinery/v1/tasks" | |||||
| ) | |||||
| // Broker - a common interface for all brokers | |||||
| type Broker interface { | |||||
| GetConfig() *config.Config | |||||
| SetRegisteredTaskNames(names []string) | |||||
| IsTaskRegistered(name string) bool | |||||
| StartConsuming(consumerTag string, concurrency int, p TaskProcessor) (bool, error) | |||||
| StopConsuming() | |||||
| Publish(ctx context.Context, task *tasks.Signature) error | |||||
| GetPendingTasks(queue string) ([]*tasks.Signature, error) | |||||
| AdjustRoutingKey(s *tasks.Signature) | |||||
| } | |||||
| // TaskProcessor - can process a delivered task | |||||
| // This will probably always be a worker instance | |||||
| type TaskProcessor interface { | |||||
| Process(signature *tasks.Signature) error | |||||
| CustomQueue() string | |||||
| } | |||||
| @@ -0,0 +1,418 @@ | |||||
| package redis | |||||
| import ( | |||||
| "bytes" | |||||
| "context" | |||||
| "encoding/json" | |||||
| "fmt" | |||||
| "sync" | |||||
| "time" | |||||
| "github.com/RichardKnop/machinery/v1/brokers/errs" | |||||
| "github.com/RichardKnop/machinery/v1/brokers/iface" | |||||
| "github.com/RichardKnop/machinery/v1/common" | |||||
| "github.com/RichardKnop/machinery/v1/config" | |||||
| "github.com/RichardKnop/machinery/v1/log" | |||||
| "github.com/RichardKnop/machinery/v1/tasks" | |||||
| "github.com/RichardKnop/redsync" | |||||
| "github.com/gomodule/redigo/redis" | |||||
| ) | |||||
| var redisDelayedTasksKey = "delayed_tasks" | |||||
| // Broker represents a Redis broker | |||||
| type Broker struct { | |||||
| common.Broker | |||||
| common.RedisConnector | |||||
| host string | |||||
| password string | |||||
| db int | |||||
| pool *redis.Pool | |||||
| consumingWG sync.WaitGroup // wait group to make sure whole consumption completes | |||||
| processingWG sync.WaitGroup // use wait group to make sure task processing completes | |||||
| delayedWG sync.WaitGroup | |||||
| // If set, path to a socket file overrides hostname | |||||
| socketPath string | |||||
| redsync *redsync.Redsync | |||||
| redisOnce sync.Once | |||||
| } | |||||
| // New creates new Broker instance | |||||
| func New(cnf *config.Config, host, password, socketPath string, db int) iface.Broker { | |||||
| b := &Broker{Broker: common.NewBroker(cnf)} | |||||
| b.host = host | |||||
| b.db = db | |||||
| b.password = password | |||||
| b.socketPath = socketPath | |||||
| return b | |||||
| } | |||||
| // StartConsuming enters a loop and waits for incoming messages | |||||
| func (b *Broker) StartConsuming(consumerTag string, concurrency int, taskProcessor iface.TaskProcessor) (bool, error) { | |||||
| b.consumingWG.Add(1) | |||||
| defer b.consumingWG.Done() | |||||
| if concurrency < 1 { | |||||
| concurrency = 1 | |||||
| } | |||||
| b.Broker.StartConsuming(consumerTag, concurrency, taskProcessor) | |||||
| conn := b.open() | |||||
| defer conn.Close() | |||||
| // Ping the server to make sure connection is live | |||||
| _, err := conn.Do("PING") | |||||
| if err != nil { | |||||
| b.GetRetryFunc()(b.GetRetryStopChan()) | |||||
| // Return err if retry is still true. | |||||
| // If retry is false, broker.StopConsuming() has been called and | |||||
| // therefore Redis might have been stopped. Return nil exit | |||||
| // StartConsuming() | |||||
| if b.GetRetry() { | |||||
| return b.GetRetry(), err | |||||
| } | |||||
| return b.GetRetry(), errs.ErrConsumerStopped | |||||
| } | |||||
| // Channel to which we will push tasks ready for processing by worker | |||||
| deliveries := make(chan []byte, concurrency) | |||||
| pool := make(chan struct{}, concurrency) | |||||
| // initialize worker pool with maxWorkers workers | |||||
| for i := 0; i < concurrency; i++ { | |||||
| pool <- struct{}{} | |||||
| } | |||||
| // A receiving goroutine keeps popping messages from the queue by BLPOP | |||||
| // If the message is valid and can be unmarshaled into a proper structure | |||||
| // we send it to the deliveries channel | |||||
| go func() { | |||||
| log.INFO.Print("[*] Waiting for messages. To exit press CTRL+C") | |||||
| for { | |||||
| select { | |||||
| // A way to stop this goroutine from b.StopConsuming | |||||
| case <-b.GetStopChan(): | |||||
| close(deliveries) | |||||
| return | |||||
| case <-pool: | |||||
| task, _ := b.nextTask(getQueue(b.GetConfig(), taskProcessor)) | |||||
| //TODO: should this error be ignored? | |||||
| if len(task) > 0 { | |||||
| deliveries <- task | |||||
| } | |||||
| pool <- struct{}{} | |||||
| } | |||||
| } | |||||
| }() | |||||
| // A goroutine to watch for delayed tasks and push them to deliveries | |||||
| // channel for consumption by the worker | |||||
| b.delayedWG.Add(1) | |||||
| go func() { | |||||
| defer b.delayedWG.Done() | |||||
| for { | |||||
| select { | |||||
| // A way to stop this goroutine from b.StopConsuming | |||||
| case <-b.GetStopChan(): | |||||
| return | |||||
| default: | |||||
| task, err := b.nextDelayedTask(redisDelayedTasksKey) | |||||
| if err != nil { | |||||
| continue | |||||
| } | |||||
| signature := new(tasks.Signature) | |||||
| decoder := json.NewDecoder(bytes.NewReader(task)) | |||||
| decoder.UseNumber() | |||||
| if err := decoder.Decode(signature); err != nil { | |||||
| log.ERROR.Print(errs.NewErrCouldNotUnmarshaTaskSignature(task, err)) | |||||
| } | |||||
| if err := b.Publish(context.Background(), signature); err != nil { | |||||
| log.ERROR.Print(err) | |||||
| } | |||||
| } | |||||
| } | |||||
| }() | |||||
| if err := b.consume(deliveries, concurrency, taskProcessor); err != nil { | |||||
| return b.GetRetry(), err | |||||
| } | |||||
| // Waiting for any tasks being processed to finish | |||||
| b.processingWG.Wait() | |||||
| return b.GetRetry(), nil | |||||
| } | |||||
| // StopConsuming quits the loop | |||||
| func (b *Broker) StopConsuming() { | |||||
| b.Broker.StopConsuming() | |||||
| // Waiting for the delayed tasks goroutine to have stopped | |||||
| b.delayedWG.Wait() | |||||
| // Waiting for consumption to finish | |||||
| b.consumingWG.Wait() | |||||
| if b.pool != nil { | |||||
| b.pool.Close() | |||||
| } | |||||
| } | |||||
| // Publish places a new message on the default queue | |||||
| func (b *Broker) Publish(ctx context.Context, signature *tasks.Signature) error { | |||||
| // Adjust routing key (this decides which queue the message will be published to) | |||||
| b.Broker.AdjustRoutingKey(signature) | |||||
| msg, err := json.Marshal(signature) | |||||
| if err != nil { | |||||
| return fmt.Errorf("JSON marshal error: %s", err) | |||||
| } | |||||
| conn := b.open() | |||||
| defer conn.Close() | |||||
| // Check the ETA signature field, if it is set and it is in the future, | |||||
| // delay the task | |||||
| if signature.ETA != nil { | |||||
| now := time.Now().UTC() | |||||
| if signature.ETA.After(now) { | |||||
| score := signature.ETA.UnixNano() | |||||
| _, err = conn.Do("ZADD", redisDelayedTasksKey, score, msg) | |||||
| return err | |||||
| } | |||||
| } | |||||
| _, err = conn.Do("RPUSH", signature.RoutingKey, msg) | |||||
| return err | |||||
| } | |||||
| // GetPendingTasks returns a slice of task signatures waiting in the queue | |||||
| func (b *Broker) GetPendingTasks(queue string) ([]*tasks.Signature, error) { | |||||
| conn := b.open() | |||||
| defer conn.Close() | |||||
| if queue == "" { | |||||
| queue = b.GetConfig().DefaultQueue | |||||
| } | |||||
| dataBytes, err := conn.Do("LRANGE", queue, 0, -1) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| results, err := redis.ByteSlices(dataBytes, err) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| taskSignatures := make([]*tasks.Signature, len(results)) | |||||
| for i, result := range results { | |||||
| signature := new(tasks.Signature) | |||||
| decoder := json.NewDecoder(bytes.NewReader(result)) | |||||
| decoder.UseNumber() | |||||
| if err := decoder.Decode(signature); err != nil { | |||||
| return nil, err | |||||
| } | |||||
| taskSignatures[i] = signature | |||||
| } | |||||
| return taskSignatures, nil | |||||
| } | |||||
| // consume takes delivered messages from the channel and manages a worker pool | |||||
| // to process tasks concurrently | |||||
| func (b *Broker) consume(deliveries <-chan []byte, concurrency int, taskProcessor iface.TaskProcessor) error { | |||||
| errorsChan := make(chan error, concurrency*2) | |||||
| pool := make(chan struct{}, concurrency) | |||||
| // init pool for Worker tasks execution, as many slots as Worker concurrency param | |||||
| go func() { | |||||
| for i := 0; i < concurrency; i++ { | |||||
| pool <- struct{}{} | |||||
| } | |||||
| }() | |||||
| for { | |||||
| select { | |||||
| case err := <-errorsChan: | |||||
| return err | |||||
| case d, open := <-deliveries: | |||||
| if !open { | |||||
| return nil | |||||
| } | |||||
| if concurrency > 0 { | |||||
| // get execution slot from pool (blocks until one is available) | |||||
| <-pool | |||||
| } | |||||
| b.processingWG.Add(1) | |||||
| // Consume the task inside a goroutine so multiple tasks | |||||
| // can be processed concurrently | |||||
| go func() { | |||||
| if err := b.consumeOne(d, taskProcessor); err != nil { | |||||
| errorsChan <- err | |||||
| } | |||||
| b.processingWG.Done() | |||||
| if concurrency > 0 { | |||||
| // give slot back to pool | |||||
| pool <- struct{}{} | |||||
| } | |||||
| }() | |||||
| } | |||||
| } | |||||
| } | |||||
| // consumeOne processes a single message using TaskProcessor | |||||
| func (b *Broker) consumeOne(delivery []byte, taskProcessor iface.TaskProcessor) error { | |||||
| signature := new(tasks.Signature) | |||||
| decoder := json.NewDecoder(bytes.NewReader(delivery)) | |||||
| decoder.UseNumber() | |||||
| if err := decoder.Decode(signature); err != nil { | |||||
| return errs.NewErrCouldNotUnmarshaTaskSignature(delivery, err) | |||||
| } | |||||
| // If the task is not registered, we requeue it, | |||||
| // there might be different workers for processing specific tasks | |||||
| if !b.IsTaskRegistered(signature.Name) { | |||||
| log.INFO.Printf("Task not registered with this worker. Requeing message: %s", delivery) | |||||
| conn := b.open() | |||||
| defer conn.Close() | |||||
| conn.Do("RPUSH", getQueue(b.GetConfig(), taskProcessor), delivery) | |||||
| return nil | |||||
| } | |||||
| log.DEBUG.Printf("Received new message: %s", delivery) | |||||
| return taskProcessor.Process(signature) | |||||
| } | |||||
| // nextTask pops next available task from the default queue | |||||
| func (b *Broker) nextTask(queue string) (result []byte, err error) { | |||||
| conn := b.open() | |||||
| defer conn.Close() | |||||
| pollPeriodMilliseconds := 1000 // default poll period for normal tasks | |||||
| if b.GetConfig().Redis != nil { | |||||
| configuredPollPeriod := b.GetConfig().Redis.NormalTasksPollPeriod | |||||
| if configuredPollPeriod > 0 { | |||||
| pollPeriodMilliseconds = configuredPollPeriod | |||||
| } | |||||
| } | |||||
| pollPeriod := time.Duration(pollPeriodMilliseconds) * time.Millisecond | |||||
| items, err := redis.ByteSlices(conn.Do("BLPOP", queue, pollPeriod.Seconds())) | |||||
| if err != nil { | |||||
| return []byte{}, err | |||||
| } | |||||
| // items[0] - the name of the key where an element was popped | |||||
| // items[1] - the value of the popped element | |||||
| if len(items) != 2 { | |||||
| return []byte{}, redis.ErrNil | |||||
| } | |||||
| result = items[1] | |||||
| return result, nil | |||||
| } | |||||
| // nextDelayedTask pops a value from the ZSET key using WATCH/MULTI/EXEC commands. | |||||
| // https://github.com/gomodule/redigo/blob/master/redis/zpop_example_test.go | |||||
| func (b *Broker) nextDelayedTask(key string) (result []byte, err error) { | |||||
| conn := b.open() | |||||
| defer conn.Close() | |||||
| defer func() { | |||||
| // Return connection to normal state on error. | |||||
| // https://redis.io/commands/discard | |||||
| if err != nil { | |||||
| conn.Do("DISCARD") | |||||
| } | |||||
| }() | |||||
| var ( | |||||
| items [][]byte | |||||
| reply interface{} | |||||
| ) | |||||
| pollPeriod := 500 // default poll period for delayed tasks | |||||
| if b.GetConfig().Redis != nil { | |||||
| configuredPollPeriod := b.GetConfig().Redis.DelayedTasksPollPeriod | |||||
| // the default period is 0, which bombards redis with requests, despite | |||||
| // our intention of doing the opposite | |||||
| if configuredPollPeriod > 0 { | |||||
| pollPeriod = configuredPollPeriod | |||||
| } | |||||
| } | |||||
| for { | |||||
| // Space out queries to ZSET so we don't bombard redis | |||||
| // server with relentless ZRANGEBYSCOREs | |||||
| time.Sleep(time.Duration(pollPeriod) * time.Millisecond) | |||||
| if _, err = conn.Do("WATCH", key); err != nil { | |||||
| return | |||||
| } | |||||
| now := time.Now().UTC().UnixNano() | |||||
| // https://redis.io/commands/zrangebyscore | |||||
| items, err = redis.ByteSlices(conn.Do( | |||||
| "ZRANGEBYSCORE", | |||||
| key, | |||||
| 0, | |||||
| now, | |||||
| "LIMIT", | |||||
| 0, | |||||
| 1, | |||||
| )) | |||||
| if err != nil { | |||||
| return | |||||
| } | |||||
| if len(items) != 1 { | |||||
| err = redis.ErrNil | |||||
| return | |||||
| } | |||||
| _ = conn.Send("MULTI") | |||||
| _ = conn.Send("ZREM", key, items[0]) | |||||
| reply, err = conn.Do("EXEC") | |||||
| if err != nil { | |||||
| return | |||||
| } | |||||
| if reply != nil { | |||||
| result = items[0] | |||||
| break | |||||
| } | |||||
| } | |||||
| return | |||||
| } | |||||
| // open returns or creates instance of Redis connection | |||||
| func (b *Broker) open() redis.Conn { | |||||
| b.redisOnce.Do(func() { | |||||
| b.pool = b.NewPool(b.socketPath, b.host, b.password, b.db, b.GetConfig().Redis, b.GetConfig().TLSConfig) | |||||
| b.redsync = redsync.New([]redsync.Pool{b.pool}) | |||||
| }) | |||||
| return b.pool.Get() | |||||
| } | |||||
| func getQueue(config *config.Config, taskProcessor iface.TaskProcessor) string { | |||||
| customQueue := taskProcessor.CustomQueue() | |||||
| if customQueue == "" { | |||||
| return config.DefaultQueue | |||||
| } | |||||
| return customQueue | |||||
| } | |||||
| @@ -0,0 +1,361 @@ | |||||
| package sqs | |||||
| import ( | |||||
| "context" | |||||
| "encoding/json" | |||||
| "errors" | |||||
| "fmt" | |||||
| "strings" | |||||
| "sync" | |||||
| "time" | |||||
| "github.com/RichardKnop/machinery/v1/brokers/iface" | |||||
| "github.com/RichardKnop/machinery/v1/common" | |||||
| "github.com/RichardKnop/machinery/v1/config" | |||||
| "github.com/RichardKnop/machinery/v1/log" | |||||
| "github.com/RichardKnop/machinery/v1/tasks" | |||||
| "github.com/aws/aws-sdk-go/aws" | |||||
| "github.com/aws/aws-sdk-go/aws/session" | |||||
| "github.com/aws/aws-sdk-go/service/sqs/sqsiface" | |||||
| awssqs "github.com/aws/aws-sdk-go/service/sqs" | |||||
| ) | |||||
| const ( | |||||
| maxAWSSQSDelay = time.Minute * 15 // Max supported SQS delay is 15 min: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_SendMessage.html | |||||
| ) | |||||
| // Broker represents a AWS SQS broker | |||||
| // There are examples on: https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/sqs-example-create-queue.html | |||||
| type Broker struct { | |||||
| common.Broker | |||||
| processingWG sync.WaitGroup // use wait group to make sure task processing completes on interrupt signal | |||||
| receivingWG sync.WaitGroup | |||||
| stopReceivingChan chan int | |||||
| sess *session.Session | |||||
| service sqsiface.SQSAPI | |||||
| queueUrl *string | |||||
| } | |||||
| // New creates new Broker instance | |||||
| func New(cnf *config.Config) iface.Broker { | |||||
| b := &Broker{Broker: common.NewBroker(cnf)} | |||||
| if cnf.SQS != nil && cnf.SQS.Client != nil { | |||||
| // Use provided *SQS client | |||||
| b.service = cnf.SQS.Client | |||||
| } else { | |||||
| // Initialize a session that the SDK will use to load credentials from the shared credentials file, ~/.aws/credentials. | |||||
| // See details on: https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html | |||||
| // Also, env AWS_REGION is also required | |||||
| b.sess = session.Must(session.NewSessionWithOptions(session.Options{ | |||||
| SharedConfigState: session.SharedConfigEnable, | |||||
| })) | |||||
| b.service = awssqs.New(b.sess) | |||||
| } | |||||
| return b | |||||
| } | |||||
| // GetPendingTasks returns a slice of task.Signatures waiting in the queue | |||||
| func (b *Broker) GetPendingTasks(queue string) ([]*tasks.Signature, error) { | |||||
| return nil, errors.New("Not implemented") | |||||
| } | |||||
| // StartConsuming enters a loop and waits for incoming messages | |||||
| func (b *Broker) StartConsuming(consumerTag string, concurrency int, taskProcessor iface.TaskProcessor) (bool, error) { | |||||
| b.Broker.StartConsuming(consumerTag, concurrency, taskProcessor) | |||||
| qURL := b.getQueueURL(taskProcessor) | |||||
| //save it so that it can be used later when attempting to delete task | |||||
| b.queueUrl = qURL | |||||
| deliveries := make(chan *awssqs.ReceiveMessageOutput, concurrency) | |||||
| pool := make(chan struct{}, concurrency) | |||||
| // initialize worker pool with maxWorkers workers | |||||
| for i := 0; i < concurrency; i++ { | |||||
| pool <- struct{}{} | |||||
| } | |||||
| b.stopReceivingChan = make(chan int) | |||||
| b.receivingWG.Add(1) | |||||
| go func() { | |||||
| defer b.receivingWG.Done() | |||||
| log.INFO.Printf("[*] Waiting for messages on queue: %s. To exit press CTRL+C\n", *qURL) | |||||
| for { | |||||
| select { | |||||
| // A way to stop this goroutine from b.StopConsuming | |||||
| case <-b.stopReceivingChan: | |||||
| close(deliveries) | |||||
| return | |||||
| case <-pool: | |||||
| output, err := b.receiveMessage(qURL) | |||||
| if err == nil && len(output.Messages) > 0 { | |||||
| deliveries <- output | |||||
| } else { | |||||
| //return back to pool right away | |||||
| pool <- struct{}{} | |||||
| if err != nil { | |||||
| log.ERROR.Printf("Queue consume error: %s", err) | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| }() | |||||
| if err := b.consume(deliveries, concurrency, taskProcessor, pool); err != nil { | |||||
| return b.GetRetry(), err | |||||
| } | |||||
| return b.GetRetry(), nil | |||||
| } | |||||
| // StopConsuming quits the loop | |||||
| func (b *Broker) StopConsuming() { | |||||
| b.Broker.StopConsuming() | |||||
| b.stopReceiving() | |||||
| // Waiting for any tasks being processed to finish | |||||
| b.processingWG.Wait() | |||||
| // Waiting for the receiving goroutine to have stopped | |||||
| b.receivingWG.Wait() | |||||
| } | |||||
| // Publish places a new message on the default queue | |||||
| func (b *Broker) Publish(ctx context.Context, signature *tasks.Signature) error { | |||||
| msg, err := json.Marshal(signature) | |||||
| if err != nil { | |||||
| return fmt.Errorf("JSON marshal error: %s", err) | |||||
| } | |||||
| // Check that signature.RoutingKey is set, if not switch to DefaultQueue | |||||
| b.AdjustRoutingKey(signature) | |||||
| MsgInput := &awssqs.SendMessageInput{ | |||||
| MessageBody: aws.String(string(msg)), | |||||
| QueueUrl: aws.String(b.GetConfig().Broker + "/" + signature.RoutingKey), | |||||
| } | |||||
| // if this is a fifo queue, there needs to be some additional parameters. | |||||
| if strings.HasSuffix(signature.RoutingKey, ".fifo") { | |||||
| // Use Machinery's signature Task UUID as SQS Message Group ID. | |||||
| MsgDedupID := signature.UUID | |||||
| MsgInput.MessageDeduplicationId = aws.String(MsgDedupID) | |||||
| // Do not Use Machinery's signature Group UUID as SQS Message Group ID, instead use BrokerMessageGroupId | |||||
| MsgGroupID := signature.BrokerMessageGroupId | |||||
| if MsgGroupID == "" { | |||||
| return fmt.Errorf("please specify BrokerMessageGroupId attribute for task Signature when submitting a task to FIFO queue") | |||||
| } | |||||
| MsgInput.MessageGroupId = aws.String(MsgGroupID) | |||||
| } | |||||
| // Check the ETA signature field, if it is set and it is in the future, | |||||
| // and is not a fifo queue, set a delay in seconds for the task. | |||||
| if signature.ETA != nil && !strings.HasSuffix(signature.RoutingKey, ".fifo") { | |||||
| now := time.Now().UTC() | |||||
| delay := signature.ETA.Sub(now) | |||||
| if delay > 0 { | |||||
| if delay > maxAWSSQSDelay { | |||||
| return errors.New("Max AWS SQS delay exceeded") | |||||
| } | |||||
| MsgInput.DelaySeconds = aws.Int64(int64(delay.Seconds())) | |||||
| } | |||||
| } | |||||
| result, err := b.service.SendMessageWithContext(ctx, MsgInput) | |||||
| if err != nil { | |||||
| log.ERROR.Printf("Error when sending a message: %v", err) | |||||
| return err | |||||
| } | |||||
| log.INFO.Printf("Sending a message successfully, the messageId is %v", *result.MessageId) | |||||
| return nil | |||||
| } | |||||
| // consume is a method which keeps consuming deliveries from a channel, until there is an error or a stop signal | |||||
| func (b *Broker) consume(deliveries <-chan *awssqs.ReceiveMessageOutput, concurrency int, taskProcessor iface.TaskProcessor, pool chan struct{}) error { | |||||
| errorsChan := make(chan error) | |||||
| for { | |||||
| whetherContinue, err := b.consumeDeliveries(deliveries, concurrency, taskProcessor, pool, errorsChan) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| if whetherContinue == false { | |||||
| return nil | |||||
| } | |||||
| } | |||||
| } | |||||
| // consumeOne is a method consumes a delivery. If a delivery was consumed successfully, it will be deleted from AWS SQS | |||||
| func (b *Broker) consumeOne(delivery *awssqs.ReceiveMessageOutput, taskProcessor iface.TaskProcessor) error { | |||||
| if len(delivery.Messages) == 0 { | |||||
| log.ERROR.Printf("received an empty message, the delivery was %v", delivery) | |||||
| return errors.New("received empty message, the delivery is " + delivery.GoString()) | |||||
| } | |||||
| sig := new(tasks.Signature) | |||||
| decoder := json.NewDecoder(strings.NewReader(*delivery.Messages[0].Body)) | |||||
| decoder.UseNumber() | |||||
| if err := decoder.Decode(sig); err != nil { | |||||
| log.ERROR.Printf("unmarshal error. the delivery is %v", delivery) | |||||
| return err | |||||
| } | |||||
| if delivery.Messages[0].ReceiptHandle != nil { | |||||
| sig.SQSReceiptHandle = *delivery.Messages[0].ReceiptHandle | |||||
| } | |||||
| // If the task is not registered return an error | |||||
| // and leave the message in the queue | |||||
| if !b.IsTaskRegistered(sig.Name) { | |||||
| return fmt.Errorf("task %s is not registered", sig.Name) | |||||
| } | |||||
| err := taskProcessor.Process(sig) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| // Delete message after successfully consuming and processing the message | |||||
| if err = b.deleteOne(delivery); err != nil { | |||||
| log.ERROR.Printf("error when deleting the delivery. delivery is %v, Error=%s", delivery, err) | |||||
| } | |||||
| return err | |||||
| } | |||||
| // deleteOne is a method delete a delivery from AWS SQS | |||||
| func (b *Broker) deleteOne(delivery *awssqs.ReceiveMessageOutput) error { | |||||
| qURL := b.defaultQueueURL() | |||||
| _, err := b.service.DeleteMessage(&awssqs.DeleteMessageInput{ | |||||
| QueueUrl: qURL, | |||||
| ReceiptHandle: delivery.Messages[0].ReceiptHandle, | |||||
| }) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| return nil | |||||
| } | |||||
| // defaultQueueURL is a method returns the default queue url | |||||
| func (b *Broker) defaultQueueURL() *string { | |||||
| if b.queueUrl != nil { | |||||
| return b.queueUrl | |||||
| } else { | |||||
| return aws.String(b.GetConfig().Broker + "/" + b.GetConfig().DefaultQueue) | |||||
| } | |||||
| } | |||||
| // receiveMessage is a method receives a message from specified queue url | |||||
| func (b *Broker) receiveMessage(qURL *string) (*awssqs.ReceiveMessageOutput, error) { | |||||
| var waitTimeSeconds int | |||||
| var visibilityTimeout *int | |||||
| if b.GetConfig().SQS != nil { | |||||
| waitTimeSeconds = b.GetConfig().SQS.WaitTimeSeconds | |||||
| visibilityTimeout = b.GetConfig().SQS.VisibilityTimeout | |||||
| } else { | |||||
| waitTimeSeconds = 0 | |||||
| } | |||||
| input := &awssqs.ReceiveMessageInput{ | |||||
| AttributeNames: []*string{ | |||||
| aws.String(awssqs.MessageSystemAttributeNameSentTimestamp), | |||||
| }, | |||||
| MessageAttributeNames: []*string{ | |||||
| aws.String(awssqs.QueueAttributeNameAll), | |||||
| }, | |||||
| QueueUrl: qURL, | |||||
| MaxNumberOfMessages: aws.Int64(1), | |||||
| WaitTimeSeconds: aws.Int64(int64(waitTimeSeconds)), | |||||
| } | |||||
| if visibilityTimeout != nil { | |||||
| input.VisibilityTimeout = aws.Int64(int64(*visibilityTimeout)) | |||||
| } | |||||
| result, err := b.service.ReceiveMessage(input) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return result, err | |||||
| } | |||||
| // initializePool is a method which initializes concurrency pool | |||||
| func (b *Broker) initializePool(pool chan struct{}, concurrency int) { | |||||
| for i := 0; i < concurrency; i++ { | |||||
| pool <- struct{}{} | |||||
| } | |||||
| } | |||||
| // consumeDeliveries is a method consuming deliveries from deliveries channel | |||||
| func (b *Broker) consumeDeliveries(deliveries <-chan *awssqs.ReceiveMessageOutput, concurrency int, taskProcessor iface.TaskProcessor, pool chan struct{}, errorsChan chan error) (bool, error) { | |||||
| select { | |||||
| case err := <-errorsChan: | |||||
| return false, err | |||||
| case d := <-deliveries: | |||||
| b.processingWG.Add(1) | |||||
| // Consume the task inside a goroutine so multiple tasks | |||||
| // can be processed concurrently | |||||
| go func() { | |||||
| if err := b.consumeOne(d, taskProcessor); err != nil { | |||||
| errorsChan <- err | |||||
| } | |||||
| b.processingWG.Done() | |||||
| if concurrency > 0 { | |||||
| // give worker back to pool | |||||
| pool <- struct{}{} | |||||
| } | |||||
| }() | |||||
| case <-b.GetStopChan(): | |||||
| return false, nil | |||||
| } | |||||
| return true, nil | |||||
| } | |||||
| // continueReceivingMessages is a method returns a continue signal | |||||
| func (b *Broker) continueReceivingMessages(qURL *string, deliveries chan *awssqs.ReceiveMessageOutput) (bool, error) { | |||||
| select { | |||||
| // A way to stop this goroutine from b.StopConsuming | |||||
| case <-b.stopReceivingChan: | |||||
| return false, nil | |||||
| default: | |||||
| output, err := b.receiveMessage(qURL) | |||||
| if err != nil { | |||||
| return true, err | |||||
| } | |||||
| if len(output.Messages) == 0 { | |||||
| return true, nil | |||||
| } | |||||
| go func() { deliveries <- output }() | |||||
| } | |||||
| return true, nil | |||||
| } | |||||
| // stopReceiving is a method sending a signal to stopReceivingChan | |||||
| func (b *Broker) stopReceiving() { | |||||
| // Stop the receiving goroutine | |||||
| b.stopReceivingChan <- 1 | |||||
| } | |||||
| // getQueueURL is a method returns that returns queueURL first by checking if custom queue was set and usign it | |||||
| // otherwise using default queueName from config | |||||
| func (b *Broker) getQueueURL(taskProcessor iface.TaskProcessor) *string { | |||||
| queueName := b.GetConfig().DefaultQueue | |||||
| if taskProcessor.CustomQueue() != "" { | |||||
| queueName = taskProcessor.CustomQueue() | |||||
| } | |||||
| return aws.String(b.GetConfig().Broker + "/" + queueName) | |||||
| } | |||||
| @@ -0,0 +1,129 @@ | |||||
| package common | |||||
| import ( | |||||
| "crypto/tls" | |||||
| "fmt" | |||||
| "github.com/streadway/amqp" | |||||
| ) | |||||
| // AMQPConnector ... | |||||
| type AMQPConnector struct{} | |||||
| // Connect opens a connection to RabbitMQ, declares an exchange, opens a channel, | |||||
| // declares and binds the queue and enables publish notifications | |||||
| func (ac *AMQPConnector) Connect(url string, tlsConfig *tls.Config, exchange, exchangeType, queueName string, queueDurable, queueDelete bool, queueBindingKey string, exchangeDeclareArgs, queueDeclareArgs, queueBindingArgs amqp.Table) (*amqp.Connection, *amqp.Channel, amqp.Queue, <-chan amqp.Confirmation, <-chan *amqp.Error, error) { | |||||
| // Connect to server | |||||
| conn, channel, err := ac.Open(url, tlsConfig) | |||||
| if err != nil { | |||||
| return nil, nil, amqp.Queue{}, nil, nil, err | |||||
| } | |||||
| if exchange != "" { | |||||
| // Declare an exchange | |||||
| if err = channel.ExchangeDeclare( | |||||
| exchange, // name of the exchange | |||||
| exchangeType, // type | |||||
| true, // durable | |||||
| false, // delete when complete | |||||
| false, // internal | |||||
| false, // noWait | |||||
| exchangeDeclareArgs, // arguments | |||||
| ); err != nil { | |||||
| return conn, channel, amqp.Queue{}, nil, nil, fmt.Errorf("Exchange declare error: %s", err) | |||||
| } | |||||
| } | |||||
| var queue amqp.Queue | |||||
| if queueName != "" { | |||||
| // Declare a queue | |||||
| queue, err = channel.QueueDeclare( | |||||
| queueName, // name | |||||
| queueDurable, // durable | |||||
| queueDelete, // delete when unused | |||||
| false, // exclusive | |||||
| false, // no-wait | |||||
| queueDeclareArgs, // arguments | |||||
| ) | |||||
| if err != nil { | |||||
| return conn, channel, amqp.Queue{}, nil, nil, fmt.Errorf("Queue declare error: %s", err) | |||||
| } | |||||
| // Bind the queue | |||||
| if err = channel.QueueBind( | |||||
| queue.Name, // name of the queue | |||||
| queueBindingKey, // binding key | |||||
| exchange, // source exchange | |||||
| false, // noWait | |||||
| queueBindingArgs, // arguments | |||||
| ); err != nil { | |||||
| return conn, channel, queue, nil, nil, fmt.Errorf("Queue bind error: %s", err) | |||||
| } | |||||
| } | |||||
| // Enable publish confirmations | |||||
| if err = channel.Confirm(false); err != nil { | |||||
| return conn, channel, queue, nil, nil, fmt.Errorf("Channel could not be put into confirm mode: %s", err) | |||||
| } | |||||
| return conn, channel, queue, channel.NotifyPublish(make(chan amqp.Confirmation, 1)), conn.NotifyClose(make(chan *amqp.Error, 1)), nil | |||||
| } | |||||
| // DeleteQueue deletes a queue by name | |||||
| func (ac *AMQPConnector) DeleteQueue(channel *amqp.Channel, queueName string) error { | |||||
| // First return value is number of messages removed | |||||
| _, err := channel.QueueDelete( | |||||
| queueName, // name | |||||
| false, // ifUnused | |||||
| false, // ifEmpty | |||||
| false, // noWait | |||||
| ) | |||||
| return err | |||||
| } | |||||
| // InspectQueue provides information about a specific queue | |||||
| func (*AMQPConnector) InspectQueue(channel *amqp.Channel, queueName string) (*amqp.Queue, error) { | |||||
| queueState, err := channel.QueueInspect(queueName) | |||||
| if err != nil { | |||||
| return nil, fmt.Errorf("Queue inspect error: %s", err) | |||||
| } | |||||
| return &queueState, nil | |||||
| } | |||||
| // Open new RabbitMQ connection | |||||
| func (ac *AMQPConnector) Open(url string, tlsConfig *tls.Config) (*amqp.Connection, *amqp.Channel, error) { | |||||
| // Connect | |||||
| // From amqp docs: DialTLS will use the provided tls.Config when it encounters an amqps:// scheme | |||||
| // and will dial a plain connection when it encounters an amqp:// scheme. | |||||
| conn, err := amqp.DialTLS(url, tlsConfig) | |||||
| if err != nil { | |||||
| return nil, nil, fmt.Errorf("Dial error: %s", err) | |||||
| } | |||||
| // Open a channel | |||||
| channel, err := conn.Channel() | |||||
| if err != nil { | |||||
| return nil, nil, fmt.Errorf("Open channel error: %s", err) | |||||
| } | |||||
| return conn, channel, nil | |||||
| } | |||||
| // Close connection | |||||
| func (ac *AMQPConnector) Close(channel *amqp.Channel, conn *amqp.Connection) error { | |||||
| if channel != nil { | |||||
| if err := channel.Close(); err != nil { | |||||
| return fmt.Errorf("Close channel error: %s", err) | |||||
| } | |||||
| } | |||||
| if conn != nil { | |||||
| if err := conn.Close(); err != nil { | |||||
| return fmt.Errorf("Close connection error: %s", err) | |||||
| } | |||||
| } | |||||
| return nil | |||||
| } | |||||
| @@ -0,0 +1,25 @@ | |||||
| package common | |||||
| import ( | |||||
| "github.com/RichardKnop/machinery/v1/config" | |||||
| ) | |||||
| // Backend represents a base backend structure | |||||
| type Backend struct { | |||||
| cnf *config.Config | |||||
| } | |||||
| // NewBackend creates new Backend instance | |||||
| func NewBackend(cnf *config.Config) Backend { | |||||
| return Backend{cnf: cnf} | |||||
| } | |||||
| // GetConfig returns config | |||||
| func (b *Backend) GetConfig() *config.Config { | |||||
| return b.cnf | |||||
| } | |||||
| // IsAMQP ... | |||||
| func (b *Backend) IsAMQP() bool { | |||||
| return false | |||||
| } | |||||
| @@ -0,0 +1,121 @@ | |||||
| package common | |||||
| import ( | |||||
| "errors" | |||||
| "github.com/RichardKnop/machinery/v1/brokers/iface" | |||||
| "github.com/RichardKnop/machinery/v1/config" | |||||
| "github.com/RichardKnop/machinery/v1/log" | |||||
| "github.com/RichardKnop/machinery/v1/retry" | |||||
| "github.com/RichardKnop/machinery/v1/tasks" | |||||
| ) | |||||
| // Broker represents a base broker structure | |||||
| type Broker struct { | |||||
| cnf *config.Config | |||||
| registeredTaskNames []string | |||||
| retry bool | |||||
| retryFunc func(chan int) | |||||
| retryStopChan chan int | |||||
| stopChan chan int | |||||
| } | |||||
| // NewBroker creates new Broker instance | |||||
| func NewBroker(cnf *config.Config) Broker { | |||||
| return Broker{ | |||||
| cnf: cnf, | |||||
| retry: true, | |||||
| stopChan: make(chan int), | |||||
| retryStopChan: make(chan int), | |||||
| } | |||||
| } | |||||
| // GetConfig returns config | |||||
| func (b *Broker) GetConfig() *config.Config { | |||||
| return b.cnf | |||||
| } | |||||
| // GetRetry ... | |||||
| func (b *Broker) GetRetry() bool { | |||||
| return b.retry | |||||
| } | |||||
| // GetRetryFunc ... | |||||
| func (b *Broker) GetRetryFunc() func(chan int) { | |||||
| return b.retryFunc | |||||
| } | |||||
| // GetRetryStopChan ... | |||||
| func (b *Broker) GetRetryStopChan() chan int { | |||||
| return b.retryStopChan | |||||
| } | |||||
| // GetStopChan ... | |||||
| func (b *Broker) GetStopChan() chan int { | |||||
| return b.stopChan | |||||
| } | |||||
| // Publish places a new message on the default queue | |||||
| func (b *Broker) Publish(signature *tasks.Signature) error { | |||||
| return errors.New("Not implemented") | |||||
| } | |||||
| // SetRegisteredTaskNames sets registered task names | |||||
| func (b *Broker) SetRegisteredTaskNames(names []string) { | |||||
| b.registeredTaskNames = names | |||||
| } | |||||
| // IsTaskRegistered returns true if the task is registered with this broker | |||||
| func (b *Broker) IsTaskRegistered(name string) bool { | |||||
| for _, registeredTaskName := range b.registeredTaskNames { | |||||
| if registeredTaskName == name { | |||||
| return true | |||||
| } | |||||
| } | |||||
| return false | |||||
| } | |||||
| // GetPendingTasks returns a slice of task.Signatures waiting in the queue | |||||
| func (b *Broker) GetPendingTasks(queue string) ([]*tasks.Signature, error) { | |||||
| return nil, errors.New("Not implemented") | |||||
| } | |||||
| // StartConsuming is a common part of StartConsuming method | |||||
| func (b *Broker) StartConsuming(consumerTag string, concurrency int, taskProcessor iface.TaskProcessor) { | |||||
| if b.retryFunc == nil { | |||||
| b.retryFunc = retry.Closure() | |||||
| } | |||||
| } | |||||
| // StopConsuming is a common part of StopConsuming | |||||
| func (b *Broker) StopConsuming() { | |||||
| // Do not retry from now on | |||||
| b.retry = false | |||||
| // Stop the retry closure earlier | |||||
| select { | |||||
| case b.retryStopChan <- 1: | |||||
| log.WARNING.Print("Stopping retry closure.") | |||||
| default: | |||||
| } | |||||
| // Notifying the stop channel stops consuming of messages | |||||
| close(b.stopChan) | |||||
| log.WARNING.Print("Stop channel") | |||||
| } | |||||
| // GetRegisteredTaskNames returns registered tasks names | |||||
| func (b *Broker) GetRegisteredTaskNames() []string { | |||||
| return b.registeredTaskNames | |||||
| } | |||||
| // AdjustRoutingKey makes sure the routing key is correct. | |||||
| // If the routing key is an empty string: | |||||
| // a) set it to binding key for direct exchange type | |||||
| // b) set it to default queue name | |||||
| func (b *Broker) AdjustRoutingKey(s *tasks.Signature) { | |||||
| if s.RoutingKey != "" { | |||||
| return | |||||
| } | |||||
| s.RoutingKey = b.GetConfig().DefaultQueue | |||||
| } | |||||
| @@ -0,0 +1,84 @@ | |||||
| package common | |||||
| import ( | |||||
| "crypto/tls" | |||||
| "time" | |||||
| "github.com/RichardKnop/machinery/v1/config" | |||||
| "github.com/gomodule/redigo/redis" | |||||
| ) | |||||
| var ( | |||||
| defaultConfig = &config.RedisConfig{ | |||||
| MaxIdle: 3, | |||||
| IdleTimeout: 240, | |||||
| ReadTimeout: 15, | |||||
| WriteTimeout: 15, | |||||
| ConnectTimeout: 15, | |||||
| NormalTasksPollPeriod: 1000, | |||||
| DelayedTasksPollPeriod: 20, | |||||
| } | |||||
| ) | |||||
| // RedisConnector ... | |||||
| type RedisConnector struct{} | |||||
| // NewPool returns a new pool of Redis connections | |||||
| func (rc *RedisConnector) NewPool(socketPath, host, password string, db int, cnf *config.RedisConfig, tlsConfig *tls.Config) *redis.Pool { | |||||
| if cnf == nil { | |||||
| cnf = defaultConfig | |||||
| } | |||||
| return &redis.Pool{ | |||||
| MaxIdle: cnf.MaxIdle, | |||||
| IdleTimeout: time.Duration(cnf.IdleTimeout) * time.Second, | |||||
| MaxActive: cnf.MaxActive, | |||||
| Wait: cnf.Wait, | |||||
| Dial: func() (redis.Conn, error) { | |||||
| c, err := rc.open(socketPath, host, password, db, cnf, tlsConfig) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| if db != 0 { | |||||
| _, err = c.Do("SELECT", db) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| } | |||||
| return c, err | |||||
| }, | |||||
| // PINGs connections that have been idle more than 10 seconds | |||||
| TestOnBorrow: func(c redis.Conn, t time.Time) error { | |||||
| if time.Since(t) < time.Duration(10*time.Second) { | |||||
| return nil | |||||
| } | |||||
| _, err := c.Do("PING") | |||||
| return err | |||||
| }, | |||||
| } | |||||
| } | |||||
| // Open a new Redis connection | |||||
| func (rc *RedisConnector) open(socketPath, host, password string, db int, cnf *config.RedisConfig, tlsConfig *tls.Config) (redis.Conn, error) { | |||||
| var opts = []redis.DialOption{ | |||||
| redis.DialDatabase(db), | |||||
| redis.DialReadTimeout(time.Duration(cnf.ReadTimeout) * time.Second), | |||||
| redis.DialWriteTimeout(time.Duration(cnf.WriteTimeout) * time.Second), | |||||
| redis.DialConnectTimeout(time.Duration(cnf.ConnectTimeout) * time.Second), | |||||
| } | |||||
| if tlsConfig != nil { | |||||
| opts = append(opts, redis.DialTLSConfig(tlsConfig), redis.DialUseTLS(true)) | |||||
| } | |||||
| if password != "" { | |||||
| opts = append(opts, redis.DialPassword(password)) | |||||
| } | |||||
| if socketPath != "" { | |||||
| return redis.Dial("unix", socketPath, opts...) | |||||
| } | |||||
| return redis.Dial("tcp", host, opts...) | |||||
| } | |||||
| @@ -0,0 +1,161 @@ | |||||
| package config | |||||
| import ( | |||||
| "crypto/tls" | |||||
| "fmt" | |||||
| "strings" | |||||
| "time" | |||||
| "cloud.google.com/go/pubsub" | |||||
| "github.com/aws/aws-sdk-go/service/dynamodb" | |||||
| "github.com/aws/aws-sdk-go/service/sqs" | |||||
| "go.mongodb.org/mongo-driver/mongo" | |||||
| ) | |||||
| const ( | |||||
| // DefaultResultsExpireIn is a default time used to expire task states and group metadata from the backend | |||||
| DefaultResultsExpireIn = 3600 | |||||
| ) | |||||
| var ( | |||||
| // Start with sensible default values | |||||
| defaultCnf = &Config{ | |||||
| Broker: "amqp://guest:guest@localhost:5672/", | |||||
| DefaultQueue: "machinery_tasks", | |||||
| ResultBackend: "amqp://guest:guest@localhost:5672/", | |||||
| ResultsExpireIn: DefaultResultsExpireIn, | |||||
| AMQP: &AMQPConfig{ | |||||
| Exchange: "machinery_exchange", | |||||
| ExchangeType: "direct", | |||||
| BindingKey: "machinery_task", | |||||
| PrefetchCount: 3, | |||||
| }, | |||||
| DynamoDB: &DynamoDBConfig{ | |||||
| TaskStatesTable: "task_states", | |||||
| GroupMetasTable: "group_metas", | |||||
| }, | |||||
| Redis: &RedisConfig{ | |||||
| MaxIdle: 3, | |||||
| IdleTimeout: 240, | |||||
| ReadTimeout: 15, | |||||
| WriteTimeout: 15, | |||||
| ConnectTimeout: 15, | |||||
| NormalTasksPollPeriod: 1000, | |||||
| DelayedTasksPollPeriod: 20, | |||||
| }, | |||||
| GCPPubSub: &GCPPubSubConfig{ | |||||
| Client: nil, | |||||
| }, | |||||
| } | |||||
| reloadDelay = time.Second * 10 | |||||
| ) | |||||
| // Config holds all configuration for our program | |||||
| type Config struct { | |||||
| Broker string `yaml:"broker" envconfig:"BROKER"` | |||||
| DefaultQueue string `yaml:"default_queue" envconfig:"DEFAULT_QUEUE"` | |||||
| ResultBackend string `yaml:"result_backend" envconfig:"RESULT_BACKEND"` | |||||
| ResultsExpireIn int `yaml:"results_expire_in" envconfig:"RESULTS_EXPIRE_IN"` | |||||
| AMQP *AMQPConfig `yaml:"amqp"` | |||||
| SQS *SQSConfig `yaml:"sqs"` | |||||
| Redis *RedisConfig `yaml:"redis"` | |||||
| GCPPubSub *GCPPubSubConfig `yaml:"-" ignored:"true"` | |||||
| MongoDB *MongoDBConfig `yaml:"-" ignored:"true"` | |||||
| TLSConfig *tls.Config | |||||
| // NoUnixSignals - when set disables signal handling in machinery | |||||
| NoUnixSignals bool `yaml:"no_unix_signals" envconfig:"NO_UNIX_SIGNALS"` | |||||
| DynamoDB *DynamoDBConfig `yaml:"dynamodb"` | |||||
| } | |||||
| // QueueBindingArgs arguments which are used when binding to the exchange | |||||
| type QueueBindingArgs map[string]interface{} | |||||
| // AMQPConfig wraps RabbitMQ related configuration | |||||
| type AMQPConfig struct { | |||||
| Exchange string `yaml:"exchange" envconfig:"AMQP_EXCHANGE"` | |||||
| ExchangeType string `yaml:"exchange_type" envconfig:"AMQP_EXCHANGE_TYPE"` | |||||
| QueueBindingArgs QueueBindingArgs `yaml:"queue_binding_args" envconfig:"AMQP_QUEUE_BINDING_ARGS"` | |||||
| BindingKey string `yaml:"binding_key" envconfig:"AMQP_BINDING_KEY"` | |||||
| PrefetchCount int `yaml:"prefetch_count" envconfig:"AMQP_PREFETCH_COUNT"` | |||||
| AutoDelete bool `yaml:"auto_delete" envconfig:"AMQP_AUTO_DELETE"` | |||||
| } | |||||
| // DynamoDBConfig wraps DynamoDB related configuration | |||||
| type DynamoDBConfig struct { | |||||
| Client *dynamodb.DynamoDB | |||||
| TaskStatesTable string `yaml:"task_states_table" envconfig:"TASK_STATES_TABLE"` | |||||
| GroupMetasTable string `yaml:"group_metas_table" envconfig:"GROUP_METAS_TABLE"` | |||||
| } | |||||
| // SQSConfig wraps SQS related configuration | |||||
| type SQSConfig struct { | |||||
| Client *sqs.SQS | |||||
| WaitTimeSeconds int `yaml:"receive_wait_time_seconds" envconfig:"SQS_WAIT_TIME_SECONDS"` | |||||
| // https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html | |||||
| // visibility timeout should default to nil to use the overall visibility timeout for the queue | |||||
| VisibilityTimeout *int `yaml:"receive_visibility_timeout" envconfig:"SQS_VISIBILITY_TIMEOUT"` | |||||
| } | |||||
| // RedisConfig ... | |||||
| type RedisConfig struct { | |||||
| // Maximum number of idle connections in the pool. | |||||
| MaxIdle int `yaml:"max_idle" envconfig:"REDIS_MAX_IDLE"` | |||||
| // Maximum number of connections allocated by the pool at a given time. | |||||
| // When zero, there is no limit on the number of connections in the pool. | |||||
| MaxActive int `yaml:"max_active" envconfig:"REDIS_MAX_ACTIVE"` | |||||
| // Close connections after remaining idle for this duration in seconds. If the value | |||||
| // is zero, then idle connections are not closed. Applications should set | |||||
| // the timeout to a value less than the server's timeout. | |||||
| IdleTimeout int `yaml:"max_idle_timeout" envconfig:"REDIS_IDLE_TIMEOUT"` | |||||
| // If Wait is true and the pool is at the MaxActive limit, then Get() waits | |||||
| // for a connection to be returned to the pool before returning. | |||||
| Wait bool `yaml:"wait" envconfig:"REDIS_WAIT"` | |||||
| // ReadTimeout specifies the timeout in seconds for reading a single command reply. | |||||
| ReadTimeout int `yaml:"read_timeout" envconfig:"REDIS_READ_TIMEOUT"` | |||||
| // WriteTimeout specifies the timeout in seconds for writing a single command. | |||||
| WriteTimeout int `yaml:"write_timeout" envconfig:"REDIS_WRITE_TIMEOUT"` | |||||
| // ConnectTimeout specifies the timeout in seconds for connecting to the Redis server when | |||||
| // no DialNetDial option is specified. | |||||
| ConnectTimeout int `yaml:"connect_timeout" envconfig:"REDIS_CONNECT_TIMEOUT"` | |||||
| // NormalTasksPollPeriod specifies the period in milliseconds when polling redis for normal tasks | |||||
| NormalTasksPollPeriod int `yaml:"normal_tasks_poll_period" envconfig:"REDIS_NORMAL_TASKS_POLL_PERIOD"` | |||||
| // DelayedTasksPollPeriod specifies the period in milliseconds when polling redis for delayed tasks | |||||
| DelayedTasksPollPeriod int `yaml:"delayed_tasks_poll_period" envconfig:"REDIS_DELAYED_TASKS_POLL_PERIOD"` | |||||
| } | |||||
| // GCPPubSubConfig wraps GCP PubSub related configuration | |||||
| type GCPPubSubConfig struct { | |||||
| Client *pubsub.Client | |||||
| MaxExtension time.Duration | |||||
| } | |||||
| // MongoDBConfig ... | |||||
| type MongoDBConfig struct { | |||||
| Client *mongo.Client | |||||
| Database string | |||||
| } | |||||
| // Decode from yaml to map (any field whose type or pointer-to-type implements | |||||
| // envconfig.Decoder can control its own deserialization) | |||||
| func (args *QueueBindingArgs) Decode(value string) error { | |||||
| pairs := strings.Split(value, ",") | |||||
| mp := make(map[string]interface{}, len(pairs)) | |||||
| for _, pair := range pairs { | |||||
| kvpair := strings.Split(pair, ":") | |||||
| if len(kvpair) != 2 { | |||||
| return fmt.Errorf("invalid map item: %q", pair) | |||||
| } | |||||
| mp[kvpair[0]] = kvpair[1] | |||||
| } | |||||
| *args = QueueBindingArgs(mp) | |||||
| return nil | |||||
| } | |||||
| @@ -0,0 +1,58 @@ | |||||
| package config | |||||
| import ( | |||||
| "time" | |||||
| "github.com/RichardKnop/machinery/v1/log" | |||||
| "github.com/kelseyhightower/envconfig" | |||||
| ) | |||||
| // NewFromEnvironment creates a config object from environment variables | |||||
| func NewFromEnvironment(keepReloading bool) (*Config, error) { | |||||
| cnf, err := fromEnvironment() | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| log.INFO.Print("Successfully loaded config from the environment") | |||||
| if keepReloading { | |||||
| // Open a goroutine to watch remote changes forever | |||||
| go func() { | |||||
| for { | |||||
| // Delay after each request | |||||
| time.Sleep(reloadDelay) | |||||
| // Attempt to reload the config | |||||
| newCnf, newErr := fromEnvironment() | |||||
| if newErr != nil { | |||||
| log.WARNING.Printf("Failed to reload config from the environment: %v", newErr) | |||||
| continue | |||||
| } | |||||
| *cnf = *newCnf | |||||
| // log.INFO.Printf("Successfully reloaded config from the environment") | |||||
| } | |||||
| }() | |||||
| } | |||||
| return cnf, nil | |||||
| } | |||||
| func fromEnvironment() (*Config, error) { | |||||
| loadedCnf, cnf := new(Config), new(Config) | |||||
| *cnf = *defaultCnf | |||||
| if err := envconfig.Process("", cnf); err != nil { | |||||
| return nil, err | |||||
| } | |||||
| if err := envconfig.Process("", loadedCnf); err != nil { | |||||
| return nil, err | |||||
| } | |||||
| if loadedCnf.AMQP == nil { | |||||
| cnf.AMQP = nil | |||||
| } | |||||
| return cnf, nil | |||||
| } | |||||
| @@ -0,0 +1,83 @@ | |||||
| package config | |||||
| import ( | |||||
| "fmt" | |||||
| "os" | |||||
| "time" | |||||
| "github.com/RichardKnop/machinery/v1/log" | |||||
| "gopkg.in/yaml.v2" | |||||
| ) | |||||
| // NewFromYaml creates a config object from YAML file | |||||
| func NewFromYaml(cnfPath string, keepReloading bool) (*Config, error) { | |||||
| cnf, err := fromFile(cnfPath) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| log.INFO.Printf("Successfully loaded config from file %s", cnfPath) | |||||
| if keepReloading { | |||||
| // Open a goroutine to watch remote changes forever | |||||
| go func() { | |||||
| for { | |||||
| // Delay after each request | |||||
| time.Sleep(reloadDelay) | |||||
| // Attempt to reload the config | |||||
| newCnf, newErr := fromFile(cnfPath) | |||||
| if newErr != nil { | |||||
| log.WARNING.Printf("Failed to reload config from file %s: %v", cnfPath, newErr) | |||||
| continue | |||||
| } | |||||
| *cnf = *newCnf | |||||
| // log.INFO.Printf("Successfully reloaded config from file %s", cnfPath) | |||||
| } | |||||
| }() | |||||
| } | |||||
| return cnf, nil | |||||
| } | |||||
| // ReadFromFile reads data from a file | |||||
| func ReadFromFile(cnfPath string) ([]byte, error) { | |||||
| file, err := os.Open(cnfPath) | |||||
| // Config file not found | |||||
| if err != nil { | |||||
| return nil, fmt.Errorf("Open file error: %s", err) | |||||
| } | |||||
| // Config file found, let's try to read it | |||||
| data := make([]byte, 1000) | |||||
| count, err := file.Read(data) | |||||
| if err != nil { | |||||
| return nil, fmt.Errorf("Read from file error: %s", err) | |||||
| } | |||||
| return data[:count], nil | |||||
| } | |||||
| func fromFile(cnfPath string) (*Config, error) { | |||||
| loadedCnf, cnf := new(Config), new(Config) | |||||
| *cnf = *defaultCnf | |||||
| data, err := ReadFromFile(cnfPath) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| if err := yaml.Unmarshal(data, cnf); err != nil { | |||||
| return nil, fmt.Errorf("Unmarshal YAML error: %s", err) | |||||
| } | |||||
| if err := yaml.Unmarshal(data, loadedCnf); err != nil { | |||||
| return nil, fmt.Errorf("Unmarshal YAML error: %s", err) | |||||
| } | |||||
| if loadedCnf.AMQP == nil { | |||||
| cnf.AMQP = nil | |||||
| } | |||||
| return cnf, nil | |||||
| } | |||||